add geolite csv parsing, improve tests, add decoding combinators
This commit is contained in:
parent
4d591380a5
commit
4bcf860fbc
@ -20,6 +20,7 @@ library
|
|||||||
Colonnade.Encoding.Text
|
Colonnade.Encoding.Text
|
||||||
Colonnade.Encoding.ByteString.Char8
|
Colonnade.Encoding.ByteString.Char8
|
||||||
Colonnade.Decoding
|
Colonnade.Decoding
|
||||||
|
Colonnade.Decoding.Text
|
||||||
Colonnade.Decoding.ByteString.Char8
|
Colonnade.Decoding.ByteString.Char8
|
||||||
Colonnade.Internal
|
Colonnade.Internal
|
||||||
Colonnade.Internal.Ap
|
Colonnade.Internal.Ap
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
{-# LANGUAGE BangPatterns #-}
|
{-# LANGUAGE BangPatterns #-}
|
||||||
module Colonnade.Decoding where
|
module Colonnade.Decoding where
|
||||||
|
|
||||||
import Colonnade.Internal (EitherWrap(..))
|
import Colonnade.Internal (EitherWrap(..),mapLeft)
|
||||||
import Colonnade.Types
|
import Colonnade.Types
|
||||||
import Data.Functor.Contravariant
|
import Data.Functor.Contravariant
|
||||||
import Data.Vector (Vector)
|
import Data.Vector (Vector)
|
||||||
@ -101,14 +101,3 @@ headedToIndexed v = getEitherWrap . go
|
|||||||
<$> EitherWrap rcurrent
|
<$> EitherWrap rcurrent
|
||||||
<*> rnext
|
<*> rnext
|
||||||
|
|
||||||
eitherMonoidAp :: Monoid a => Either a (b -> c) -> Either a b -> Either a c
|
|
||||||
eitherMonoidAp = go where
|
|
||||||
go (Left a1) (Left a2) = Left (mappend a1 a2)
|
|
||||||
go (Left a1) (Right _) = Left a1
|
|
||||||
go (Right _) (Left a2) = Left a2
|
|
||||||
go (Right f) (Right b) = Right (f b)
|
|
||||||
|
|
||||||
mapLeft :: (a -> b) -> Either a c -> Either b c
|
|
||||||
mapLeft _ (Right a) = Right a
|
|
||||||
mapLeft f (Left a) = Left (f a)
|
|
||||||
|
|
||||||
|
|||||||
47
colonnade/src/Colonnade/Decoding/Text.hs
Normal file
47
colonnade/src/Colonnade/Decoding/Text.hs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
module Colonnade.Decoding.Text where
|
||||||
|
|
||||||
|
import Prelude hiding (map)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
import qualified Data.Text.Read as TextRead
|
||||||
|
|
||||||
|
char :: Text -> Either String Char
|
||||||
|
char t = case Text.length t of
|
||||||
|
1 -> Right (Text.head t)
|
||||||
|
0 -> Left "cannot decode Char from empty text"
|
||||||
|
_ -> Left "cannot decode Char from multi-character text"
|
||||||
|
|
||||||
|
text :: Text -> Either String Text
|
||||||
|
text = Right
|
||||||
|
|
||||||
|
int :: Text -> Either String Int
|
||||||
|
int t = do
|
||||||
|
(a,tRem) <- TextRead.decimal t
|
||||||
|
if Text.null tRem
|
||||||
|
then Right a
|
||||||
|
else Left "found extra characters after int"
|
||||||
|
|
||||||
|
trueFalse :: Text -> Text -> Text -> Either String Bool
|
||||||
|
trueFalse t f txt
|
||||||
|
| txt == t = Right True
|
||||||
|
| txt == f = Right False
|
||||||
|
| otherwise = Left $ concat
|
||||||
|
["must be [", Text.unpack t, "] or [", Text.unpack f, "]"]
|
||||||
|
|
||||||
|
-- | This refers to the 'TextRead.Reader' from @Data.Text.Read@, not
|
||||||
|
-- to the @Reader@ monad.
|
||||||
|
fromReader :: TextRead.Reader a -> Text -> Either String a
|
||||||
|
fromReader f t = do
|
||||||
|
(a,tRem) <- f t
|
||||||
|
if Text.null tRem
|
||||||
|
then Right a
|
||||||
|
else Left "found extra characters at end of text"
|
||||||
|
|
||||||
|
optional :: (Text -> Either String a) -> Text -> Either String (Maybe a)
|
||||||
|
optional f t = if Text.null t
|
||||||
|
then Right Nothing
|
||||||
|
else fmap Just (f t)
|
||||||
|
|
||||||
|
map :: (a -> b) -> (Text -> Either String a) -> Text -> Either String b
|
||||||
|
map f g t = fmap f (g t)
|
||||||
|
|
||||||
@ -17,3 +17,8 @@ int = LText.toStrict
|
|||||||
text :: Text -> Text
|
text :: Text -> Text
|
||||||
text = id
|
text = id
|
||||||
|
|
||||||
|
bool :: Bool -> Text
|
||||||
|
bool x = case x of
|
||||||
|
True -> Text.pack "true"
|
||||||
|
False -> Text.pack "false"
|
||||||
|
|
||||||
|
|||||||
@ -12,3 +12,6 @@ instance Monoid a => Applicative (EitherWrap a) where
|
|||||||
EitherWrap (Right _) <*> EitherWrap (Left a2) = EitherWrap (Left a2)
|
EitherWrap (Right _) <*> EitherWrap (Left a2) = EitherWrap (Left a2)
|
||||||
EitherWrap (Right f) <*> EitherWrap (Right b) = EitherWrap (Right (f b))
|
EitherWrap (Right f) <*> EitherWrap (Right b) = EitherWrap (Right (f b))
|
||||||
|
|
||||||
|
mapLeft :: (a -> b) -> Either a c -> Either b c
|
||||||
|
mapLeft _ (Right a) = Right a
|
||||||
|
mapLeft f (Left a) = Left (f a)
|
||||||
|
|||||||
@ -38,7 +38,7 @@ data Indexed f a = Indexed
|
|||||||
data HeadingErrors content = HeadingErrors
|
data HeadingErrors content = HeadingErrors
|
||||||
{ headingErrorsMissing :: Vector content -- ^ headers that were missing
|
{ headingErrorsMissing :: Vector content -- ^ headers that were missing
|
||||||
, headingErrorsDuplicate :: Vector (content,Int) -- ^ headers that occurred more than once
|
, headingErrorsDuplicate :: Vector (content,Int) -- ^ headers that occurred more than once
|
||||||
} deriving (Show,Read)
|
} deriving (Show,Read,Eq)
|
||||||
|
|
||||||
instance (Show content, Typeable content) => Exception (HeadingErrors content)
|
instance (Show content, Typeable content) => Exception (HeadingErrors content)
|
||||||
|
|
||||||
@ -51,27 +51,32 @@ data DecodingCellError f content = DecodingCellError
|
|||||||
{ decodingCellErrorContent :: !content
|
{ decodingCellErrorContent :: !content
|
||||||
, decodingCellErrorHeader :: !(Indexed f content)
|
, decodingCellErrorHeader :: !(Indexed f content)
|
||||||
, decodingCellErrorMessage :: !String
|
, decodingCellErrorMessage :: !String
|
||||||
} deriving (Show,Read)
|
} deriving (Show,Read,Eq)
|
||||||
|
|
||||||
-- instance (Show (f content), Typeable content) => Exception (DecodingError f content)
|
-- instance (Show (f content), Typeable content) => Exception (DecodingError f content)
|
||||||
|
|
||||||
newtype DecodingCellErrors f content = DecodingCellErrors
|
newtype DecodingCellErrors f content = DecodingCellErrors
|
||||||
{ getDecodingCellErrors :: Vector (DecodingCellError f content)
|
{ getDecodingCellErrors :: Vector (DecodingCellError f content)
|
||||||
} deriving (Monoid,Show,Read)
|
} deriving (Monoid,Show,Read,Eq)
|
||||||
|
|
||||||
-- newtype ParseRowError = ParseRowError String
|
-- newtype ParseRowError = ParseRowError String
|
||||||
|
|
||||||
|
-- TODO: rewrite the instances for this by hand. They
|
||||||
|
-- currently use FlexibleContexts.
|
||||||
data DecodingRowError f content = DecodingRowError
|
data DecodingRowError f content = DecodingRowError
|
||||||
{ decodingRowErrorRow :: !Int
|
{ decodingRowErrorRow :: !Int
|
||||||
, decodingRowErrorError :: !(RowError f content)
|
, decodingRowErrorError :: !(RowError f content)
|
||||||
}
|
} deriving (Show,Read,Eq)
|
||||||
|
|
||||||
|
-- TODO: rewrite the instances for this by hand. They
|
||||||
|
-- currently use FlexibleContexts.
|
||||||
data RowError f content
|
data RowError f content
|
||||||
= RowErrorParse !String -- ^ Error occurred parsing the document into cells
|
= RowErrorParse !String -- ^ Error occurred parsing the document into cells
|
||||||
| RowErrorDecode !(DecodingCellErrors f content) -- ^ Error decoding the content
|
| RowErrorDecode !(DecodingCellErrors f content) -- ^ Error decoding the content
|
||||||
| RowErrorSize !Int !Int -- ^ Wrong number of cells in the row
|
| RowErrorSize !Int !Int -- ^ Wrong number of cells in the row
|
||||||
| RowErrorHeading !(HeadingErrors content)
|
| RowErrorHeading !(HeadingErrors content)
|
||||||
| RowErrorMinSize !Int !Int
|
| RowErrorMinSize !Int !Int
|
||||||
|
deriving (Show,Read,Eq)
|
||||||
|
|
||||||
-- instance (Show (f content), Typeable content) => Exception (DecodingErrors f content)
|
-- instance (Show (f content), Typeable content) => Exception (DecodingErrors f content)
|
||||||
|
|
||||||
|
|||||||
30
geolite-csv/LICENSE
Normal file
30
geolite-csv/LICENSE
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
Copyright Andrew Martin (c) 2016
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following
|
||||||
|
disclaimer in the documentation and/or other materials provided
|
||||||
|
with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of Andrew Martin nor the names of other
|
||||||
|
contributors may be used to endorse or promote products derived
|
||||||
|
from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
2
geolite-csv/Setup.hs
Normal file
2
geolite-csv/Setup.hs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import Distribution.Simple
|
||||||
|
main = defaultMain
|
||||||
11
geolite-csv/data/GeoLite2-City-Blocks-IPv4.small.csv
Normal file
11
geolite-csv/data/GeoLite2-City-Blocks-IPv4.small.csv
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
network,geoname_id,registered_country_geoname_id,represented_country_geoname_id,is_anonymous_proxy,is_satellite_provider,postal_code,latitude,longitude,accuracy_radius
|
||||||
|
24.165.56.0/22,5848280,6252001,,0,0,96746,22.0837,-159.3553,10
|
||||||
|
78.146.173.128/25,2655583,2635167,,0,0,DL14,54.6500,-1.6667,20
|
||||||
|
121.211.108.0/23,2160386,2077456,,0,0,2040,-33.8833,151.1500,5
|
||||||
|
69.74.43.16/30,6252001,6252001,,0,0,,37.7510,-97.8220,1000
|
||||||
|
77.128.35.136/30,3034803,3017382,,0,0,57450,49.0667,6.8333,20
|
||||||
|
90.54.234.0/24,2977062,3017382,,0,0,49320,47.3944,-0.4357,50
|
||||||
|
77.193.41.175/32,3018587,3017382,,0,0,78810,48.8700,1.9740,1
|
||||||
|
58.188.32.0/24,1861060,1861060,,0,0,,35.6900,139.6900,500
|
||||||
|
87.81.232.0/24,2635167,2635167,,0,0,,51.4964,-0.1224,200
|
||||||
|
88.191.56.0/22,2988507,3017382,,0,0,75001,48.8667,2.3333,500
|
||||||
|
21
geolite-csv/data/GeoLite2-City-Locations-en.small.csv
Normal file
21
geolite-csv/data/GeoLite2-City-Locations-en.small.csv
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_name,subdivision_1_iso_code,subdivision_1_name,subdivision_2_iso_code,subdivision_2_name,city_name,metro_code,time_zone
|
||||||
|
2653810,en,EU,Europe,GB,"United Kingdom",SCT,Scotland,GLG,"Glasgow City",Cardonald,,Europe/London
|
||||||
|
2832529,en,EU,Europe,DE,Germany,RP,Rheinland-Pfalz,,,Siefersheim,,Europe/Berlin
|
||||||
|
2885499,en,EU,Europe,DE,Germany,MV,Mecklenburg-Vorpommern,,,Koerchow,,Europe/Berlin
|
||||||
|
550870,en,EU,Europe,RU,Russia,NIZ,"Nizhegorodskaya Oblast'",,,Khabarskoye,,Europe/Moscow
|
||||||
|
766583,en,EU,Europe,PL,Poland,LU,"Lublin Voivodeship",,,Leczna,,Europe/Warsaw
|
||||||
|
2608246,en,EU,Europe,AT,Austria,1,Burgenland,,,"Neuhaus am Klausenbach",,Europe/Vienna
|
||||||
|
5121765,en,NA,"North America",US,"United States",NY,"New York",,,Ilion,526,America/New_York
|
||||||
|
2935825,en,EU,Europe,DE,Germany,NW,"North Rhine-Westphalia",,,Dormagen,,Europe/Berlin
|
||||||
|
3165189,en,EU,Europe,IT,Italy,36,"Friuli Venezia Giulia",UD,"Provincia di Udine",Tricesimo,,Europe/Rome
|
||||||
|
4564070,en,NA,"North America",PR,"Puerto Rico",,,,,Culebra,,America/Puerto_Rico
|
||||||
|
2993759,en,EU,Europe,FR,France,U,"Provence-Alpes-Côte d'Azur",13,Bouches-du-Rhône,Miramas-le-Vieux,,Europe/Paris
|
||||||
|
5861117,en,NA,"North America",US,"United States",AK,Alaska,,,"Dutch Harbor",743,America/Adak
|
||||||
|
4375229,en,NA,"North America",US,"United States",MO,Missouri,,,Ashland,604,America/Chicago
|
||||||
|
2946980,en,EU,Europe,DE,Germany,SN,Saxony,,,Boehlen,,Europe/Berlin
|
||||||
|
3156470,en,EU,Europe,NO,Norway,02,Akershus,,,Frogner,,Europe/Oslo
|
||||||
|
3166193,en,EU,Europe,IT,Italy,36,"Friuli Venezia Giulia",GO,"Provincia di Gorizia",Staranzano,,Europe/Rome
|
||||||
|
4913742,en,NA,"North America",US,"United States",IL,Illinois,,,Tiskilwa,675,America/Chicago
|
||||||
|
4853511,en,NA,"North America",US,"United States",IA,Iowa,,,Dayton,679,America/Chicago
|
||||||
|
480876,en,EU,Europe,RU,Russia,ROS,Rostov,,,Tsimlyansk,,Europe/Moscow
|
||||||
|
3000119,en,EU,Europe,FR,France,89,Yonne,,,"Les Ormes",,Europe/Paris
|
||||||
|
21
geolite-csv/data/GeoLite2-City-Locations-ja.small.csv
Normal file
21
geolite-csv/data/GeoLite2-City-Locations-ja.small.csv
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
geoname_id,locale_code,continent_code,continent_name,country_iso_code,country_name,subdivision_1_iso_code,subdivision_1_name,subdivision_2_iso_code,subdivision_2_name,city_name,metro_code,time_zone
|
||||||
|
1260633,ja,AS,"アジア",IN,"インド",AP,"アーンドラ・プラデーシュ州",,,,,Asia/Kolkata
|
||||||
|
4765167,ja,NA,"北アメリカ",US,"アメリカ合衆国",VA,"バージニア州",,,,573,America/New_York
|
||||||
|
2703330,ja,EU,"ヨーロッパ",SE,"スウェーデン王国",Z,,,,,,Europe/Stockholm
|
||||||
|
535886,ja,EU,"ヨーロッパ",RU,"ロシア",STA,,,,,,Europe/Moscow
|
||||||
|
2989001,ja,EU,"ヨーロッパ",FR,"フランス共和国",F,,28,,,,Europe/Paris
|
||||||
|
3183178,ja,EU,"ヨーロッパ",IT,"イタリア共和国",75,"プッリャ州",BA,,"アルタムーラ",,Europe/Rome
|
||||||
|
3012956,ja,EU,"ヨーロッパ",FR,"フランス共和国",67,,,,,,Europe/Paris
|
||||||
|
4189157,ja,NA,"北アメリカ",US,"アメリカ合衆国",GA,"ジョージア州",,,,524,America/New_York
|
||||||
|
2758965,ja,EU,"ヨーロッパ",NL,"オランダ王国",ZE,,,,,,Europe/Amsterdam
|
||||||
|
3570412,ja,NA,"北アメリカ",MQ,"マルティニーク島",,,,,,,America/Martinique
|
||||||
|
3095604,ja,EU,"ヨーロッパ",PL,"ポーランド共和国",MZ,"マゾフシェ県",,,,,Europe/Warsaw
|
||||||
|
3070865,ja,EU,"ヨーロッパ",CZ,"チェコ共和国",ST,"中央ボヘミア州",,,,,Europe/Prague
|
||||||
|
2636062,ja,EU,"ヨーロッパ",GB,"イギリス",ENG,"イングランド",SRY,,,,Europe/London
|
||||||
|
3019338,ja,EU,"ヨーロッパ",FR,"フランス共和国",57,,,,,,Europe/Paris
|
||||||
|
2865603,ja,EU,"ヨーロッパ",DE,"ドイツ連邦共和国",BY,"バイエルン州",,,"ノイエンマルクト",,Europe/Berlin
|
||||||
|
2930628,ja,EU,"ヨーロッパ",DE,"ドイツ連邦共和国",HE,,,,,,Europe/Berlin
|
||||||
|
2976283,ja,EU,"ヨーロッパ",FR,"フランス共和国",01,,,,,,Europe/Paris
|
||||||
|
4062424,ja,NA,"北アメリカ",US,"アメリカ合衆国",AL,"アラバマ州",,,,575,America/Chicago
|
||||||
|
4461574,ja,NA,"北アメリカ",US,"アメリカ合衆国",NC,"ノースカロライナ州",,,"コンコード",517,America/New_York
|
||||||
|
1279945,ja,AS,"アジア",CN,"中国",62,,,,"酒泉市",,Asia/Shanghai
|
||||||
|
51
geolite-csv/geolite-csv.cabal
Normal file
51
geolite-csv/geolite-csv.cabal
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
name: geolite-csv
|
||||||
|
version: 0.1.0
|
||||||
|
synopsis: Initial project template from stack
|
||||||
|
description: Please see README.md
|
||||||
|
homepage: https://github.com/andrewthad/colonnade
|
||||||
|
license: BSD3
|
||||||
|
license-file: LICENSE
|
||||||
|
author: Andrew Martin
|
||||||
|
maintainer: andrew.thaddeus@gmail.com
|
||||||
|
copyright: 2016 Andrew Martin
|
||||||
|
category: web
|
||||||
|
build-type: Simple
|
||||||
|
-- extra-source-files:
|
||||||
|
cabal-version: >=1.10
|
||||||
|
|
||||||
|
library
|
||||||
|
hs-source-dirs: src
|
||||||
|
exposed-modules:
|
||||||
|
Geolite.Types
|
||||||
|
Geolite.Csv
|
||||||
|
build-depends:
|
||||||
|
base >= 4.7 && < 5
|
||||||
|
, colonnade
|
||||||
|
, siphon
|
||||||
|
, ip >= 0.8.4
|
||||||
|
, text
|
||||||
|
, pipes
|
||||||
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
test-suite geolite-csv-test
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
hs-source-dirs: test
|
||||||
|
main-is: Spec.hs
|
||||||
|
build-depends:
|
||||||
|
base
|
||||||
|
, geolite-csv
|
||||||
|
, siphon
|
||||||
|
, colonnade
|
||||||
|
, test-framework
|
||||||
|
, text
|
||||||
|
, pipes
|
||||||
|
, HUnit
|
||||||
|
, test-framework-hunit
|
||||||
|
, pipes-bytestring
|
||||||
|
, pipes-text
|
||||||
|
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||||
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
source-repository head
|
||||||
|
type: git
|
||||||
|
location: https://github.com/andrewthad/colonnade
|
||||||
54
geolite-csv/src/Geolite/Csv.hs
Normal file
54
geolite-csv/src/Geolite/Csv.hs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Geolite.Csv where
|
||||||
|
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Pipes (Pipe)
|
||||||
|
import Colonnade.Types
|
||||||
|
import Geolite.Types
|
||||||
|
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
import qualified Net.IPv4.Range.Text as IPv4RangeText
|
||||||
|
import qualified Data.Text.Read as TextRead
|
||||||
|
import qualified Siphon.Decoding as SD
|
||||||
|
import qualified Siphon.Content as SC
|
||||||
|
import qualified Colonnade.Decoding.Text as CDT
|
||||||
|
import qualified Colonnade.Decoding as CD
|
||||||
|
|
||||||
|
cities :: Monad m => Pipe Text City m (DecodingRowError Headed Text)
|
||||||
|
cities = SD.headedPipe SC.text decodingCity
|
||||||
|
|
||||||
|
blocks :: Monad m => Pipe Text Block m (DecodingRowError Headed Text)
|
||||||
|
blocks = SD.headedPipe SC.text decodingBlock
|
||||||
|
|
||||||
|
decodingCity :: Decoding Headed Text City
|
||||||
|
decodingCity = City
|
||||||
|
<$> fmap GeonameId (CD.headed "geoname_id" CDT.int)
|
||||||
|
<*> CD.headed "locale_code" CDT.text
|
||||||
|
<*> CD.headed "continent_code" CDT.text
|
||||||
|
<*> CD.headed "continent_name" CDT.text
|
||||||
|
<*> CD.headed "country_iso_code" CDT.text
|
||||||
|
<*> CD.headed "country_name" CDT.text
|
||||||
|
<*> CD.headed "subdivision_1_iso_code" CDT.text
|
||||||
|
<*> CD.headed "subdivision_1_name" CDT.text
|
||||||
|
<*> CD.headed "subdivision_2_iso_code" CDT.text
|
||||||
|
<*> CD.headed "subdivision_2_name" CDT.text
|
||||||
|
<*> CD.headed "metro_code" (CDT.optional CDT.int)
|
||||||
|
<*> CD.headed "time_zone" CDT.text
|
||||||
|
|
||||||
|
decodingBlock :: Decoding Headed Text Block
|
||||||
|
decodingBlock = Block
|
||||||
|
<$> CD.headed "network" IPv4RangeText.decodeEither
|
||||||
|
<*> CD.headed "geoname_id" (CDT.map GeonameId CDT.int)
|
||||||
|
<*> CD.headed "registered_country_geoname_id"
|
||||||
|
(CDT.map GeonameId CDT.int)
|
||||||
|
<*> CD.headed "represented_country_geoname_id"
|
||||||
|
(CDT.optional $ CDT.map GeonameId CDT.int)
|
||||||
|
<*> CD.headed "is_anonymous_proxy" (CDT.trueFalse "1" "0")
|
||||||
|
<*> CD.headed "is_satellite_provider" (CDT.trueFalse "1" "0")
|
||||||
|
<*> CD.headed "postal_code" CDT.text
|
||||||
|
<*> CD.headed "latitude" (CDT.fromReader TextRead.rational)
|
||||||
|
<*> CD.headed "longitude" (CDT.fromReader TextRead.rational)
|
||||||
|
<*> CD.headed "accuracy_radius" CDT.int
|
||||||
|
|
||||||
|
|
||||||
42
geolite-csv/src/Geolite/Types.hs
Normal file
42
geolite-csv/src/Geolite/Types.hs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
module Geolite.Types where
|
||||||
|
|
||||||
|
import Net.Types (IPv4Range)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Data.Fixed
|
||||||
|
|
||||||
|
data E4
|
||||||
|
|
||||||
|
instance HasResolution E4 where
|
||||||
|
resolution _ = 4
|
||||||
|
|
||||||
|
newtype GeonameId = GeonameId { getGeonameId :: Int }
|
||||||
|
deriving (Show,Read,Eq,Ord)
|
||||||
|
|
||||||
|
data City = City
|
||||||
|
{ cityGeonameId :: GeonameId
|
||||||
|
, cityLocaleCode :: Text
|
||||||
|
, cityContinentCode :: Text
|
||||||
|
, cityContinentName :: Text
|
||||||
|
, cityCountryIsoCode :: Text
|
||||||
|
, cityCountryName :: Text
|
||||||
|
, citySubdivision1IsoCode :: Text
|
||||||
|
, citySubdivision1Name :: Text
|
||||||
|
, citySubdivision2IsoCode :: Text
|
||||||
|
, citySubdivision2Name :: Text
|
||||||
|
, cityMetroCode :: Maybe Int
|
||||||
|
, cityTimeZone :: Text
|
||||||
|
} deriving (Show,Read,Eq,Ord)
|
||||||
|
|
||||||
|
data Block = Block
|
||||||
|
{ blockNetwork :: IPv4Range
|
||||||
|
, blockGeonameId :: GeonameId
|
||||||
|
, blockRegisteredCountryGeonameId :: GeonameId
|
||||||
|
, blockRepresentedCountryGeonameId :: Maybe GeonameId
|
||||||
|
, blockIsAnonymousProxy :: Bool
|
||||||
|
, blockIsSatelliteProvider :: Bool
|
||||||
|
, blockPostalCode :: Text
|
||||||
|
, blockLatitude :: Fixed E4
|
||||||
|
, blockLongitude :: Fixed E4
|
||||||
|
, blockAccuracyRadius :: Int
|
||||||
|
} deriving (Show,Read,Eq,Ord)
|
||||||
|
|
||||||
46
geolite-csv/test/Spec.hs
Normal file
46
geolite-csv/test/Spec.hs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Main (main) where
|
||||||
|
|
||||||
|
import Test.HUnit (Assertion,(@?=))
|
||||||
|
import Test.Framework (defaultMain, testGroup, Test)
|
||||||
|
import Test.Framework.Providers.HUnit (testCase)
|
||||||
|
import Geolite.Csv (cities,blocks)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Colonnade.Types
|
||||||
|
import Siphon.Types
|
||||||
|
import Data.Functor.Identity
|
||||||
|
import System.IO (withFile,IOMode(ReadMode))
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
import qualified Pipes.Prelude as Pipes
|
||||||
|
import qualified Pipes.ByteString as PB
|
||||||
|
import qualified Pipes.Text.Encoding as PT
|
||||||
|
import qualified Siphon.Decoding as SD
|
||||||
|
import Pipes
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = defaultMain tests
|
||||||
|
|
||||||
|
tests :: [Test]
|
||||||
|
tests =
|
||||||
|
[ testGroup "Geolite CSV Decoding"
|
||||||
|
[ testCase "Network Blocks" $ streamFileWith
|
||||||
|
"data/GeoLite2-City-Blocks-IPv4.small.csv"
|
||||||
|
blocks
|
||||||
|
, testCase "English City Locations" $ streamFileWith
|
||||||
|
"data/GeoLite2-City-Locations-en.small.csv"
|
||||||
|
cities
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
streamFileWith ::
|
||||||
|
String
|
||||||
|
-> Pipe Text a IO (DecodingRowError Headed Text)
|
||||||
|
-> Assertion
|
||||||
|
streamFileWith filename decodingPipe = do
|
||||||
|
r <- withFile filename ReadMode $ \h -> runEffect $
|
||||||
|
fmap SD.csvResultFromEither (PT.decode (PT.utf8 . PT.eof) $ PB.fromHandle h)
|
||||||
|
>-> fmap SD.csvResultFromDecodingRowError decodingPipe
|
||||||
|
>-> Pipes.drain
|
||||||
|
r @?= CsvResultSuccess
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
name: reflex-dom-colonnade
|
name: reflex-dom-colonnade
|
||||||
version: 0.3
|
version: 0.4
|
||||||
synopsis: Use colonnade with reflex-dom
|
synopsis: Use colonnade with reflex-dom
|
||||||
description: Please see README.md
|
description: Please see README.md
|
||||||
homepage: https://github.com/andrewthad/colonnade#readme
|
homepage: https://github.com/andrewthad/colonnade#readme
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
{-# LANGUAGE DeriveFunctor #-}
|
||||||
|
|
||||||
module Reflex.Dom.Colonnade where
|
module Reflex.Dom.Colonnade where
|
||||||
|
|
||||||
import Colonnade.Types
|
import Colonnade.Types
|
||||||
@ -18,7 +20,10 @@ cell = Cell Map.empty
|
|||||||
data Cell m b = Cell
|
data Cell m b = Cell
|
||||||
{ cellAttrs :: !(Map String String)
|
{ cellAttrs :: !(Map String String)
|
||||||
, cellContents :: !(m b)
|
, cellContents :: !(m b)
|
||||||
}
|
} deriving (Functor)
|
||||||
|
|
||||||
|
-- instance Functor (Cell m) where
|
||||||
|
-- fmap f (a
|
||||||
|
|
||||||
basic :: (MonadWidget t m, Foldable f)
|
basic :: (MonadWidget t m, Foldable f)
|
||||||
=> Map String String -- ^ Table element attributes
|
=> Map String String -- ^ Table element attributes
|
||||||
@ -29,7 +34,7 @@ basic tableAttrs as encoding = do
|
|||||||
elAttr "table" tableAttrs $ do
|
elAttr "table" tableAttrs $ do
|
||||||
theadBuild encoding
|
theadBuild encoding
|
||||||
el "tbody" $ forM_ as $ \a -> do
|
el "tbody" $ forM_ as $ \a -> do
|
||||||
el "tr" $ mapM_ (Encoding.runRowMonadic encoding (elFromCell "td")) as
|
el "tr" $ Encoding.runRowMonadic encoding (elFromCell "td") a
|
||||||
|
|
||||||
elFromCell :: MonadWidget t m => String -> Cell m b -> m b
|
elFromCell :: MonadWidget t m => String -> Cell m b -> m b
|
||||||
elFromCell name (Cell attrs contents) = elAttr name attrs contents
|
elFromCell name (Cell attrs contents) = elAttr name attrs contents
|
||||||
|
|||||||
@ -23,6 +23,7 @@ library
|
|||||||
Siphon.Encoding
|
Siphon.Encoding
|
||||||
Siphon.Decoding
|
Siphon.Decoding
|
||||||
Siphon.Internal
|
Siphon.Internal
|
||||||
|
Siphon.Internal.Text
|
||||||
build-depends:
|
build-depends:
|
||||||
base >= 4.7 && < 5
|
base >= 4.7 && < 5
|
||||||
, colonnade
|
, colonnade
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
module Siphon.Content
|
module Siphon.Content
|
||||||
( byteStringChar8
|
( byteStringChar8
|
||||||
|
, text
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Siphon.Internal
|
import Siphon.Internal (byteStringChar8)
|
||||||
|
import Siphon.Internal.Text (text)
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,13 @@ mkParseError i ctxs msg = id
|
|||||||
, "]"
|
, "]"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
csvResultFromEither :: Either (Producer ByteString m ()) () -> CsvResult f c
|
||||||
|
csvResultFromEither (Left _) = CsvResultTextDecodeError
|
||||||
|
csvResultFromEither (Right ()) = CsvResultSuccess
|
||||||
|
|
||||||
|
csvResultFromDecodingRowError :: DecodingRowError f c -> CsvResult f c
|
||||||
|
csvResultFromDecodingRowError = CsvResultDecodeError
|
||||||
|
|
||||||
-- | This is seldom useful but is included for completeness.
|
-- | This is seldom useful but is included for completeness.
|
||||||
headlessPipe :: Monad m
|
headlessPipe :: Monad m
|
||||||
=> Siphon c
|
=> Siphon c
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import qualified Data.Vector as V
|
|||||||
import qualified Data.ByteString as B
|
import qualified Data.ByteString as B
|
||||||
import qualified Data.ByteString.Lazy as LByteString
|
import qualified Data.ByteString.Lazy as LByteString
|
||||||
import qualified Data.ByteString.Builder as Builder
|
import qualified Data.ByteString.Builder as Builder
|
||||||
|
import qualified Data.Text as T
|
||||||
import Data.Word (Word8)
|
import Data.Word (Word8)
|
||||||
import Data.Vector (Vector)
|
import Data.Vector (Vector)
|
||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
|
|||||||
189
siphon/src/Siphon/Internal/Text.hs
Normal file
189
siphon/src/Siphon/Internal/Text.hs
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
{-# LANGUAGE BangPatterns #-}
|
||||||
|
|
||||||
|
module Siphon.Internal.Text where
|
||||||
|
|
||||||
|
import Siphon.Types
|
||||||
|
|
||||||
|
import Control.Applicative (optional)
|
||||||
|
import Data.Attoparsec.Text (char, endOfInput, string)
|
||||||
|
import qualified Data.Attoparsec.Text as A
|
||||||
|
import qualified Data.Attoparsec.Text.Lazy as AL
|
||||||
|
import qualified Data.Attoparsec.Zepto as Z
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
import qualified Data.Vector as V
|
||||||
|
import qualified Data.Text.Lazy as LText
|
||||||
|
import qualified Data.Text.Lazy.Builder as Builder
|
||||||
|
import Data.Text.Lazy.Builder (Builder)
|
||||||
|
import Data.Word (Word8)
|
||||||
|
import Data.Vector (Vector)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Data.Coerce (coerce)
|
||||||
|
import Siphon.Types
|
||||||
|
|
||||||
|
import Control.Applicative
|
||||||
|
import Data.Monoid
|
||||||
|
|
||||||
|
text :: Siphon Text
|
||||||
|
text = Siphon
|
||||||
|
escape
|
||||||
|
encodeRow
|
||||||
|
(A.parse (row comma))
|
||||||
|
Text.null
|
||||||
|
|
||||||
|
encodeRow :: Vector (Escaped Text) -> Text
|
||||||
|
encodeRow = id
|
||||||
|
. flip Text.append (Text.singleton newline)
|
||||||
|
. Text.intercalate (Text.singleton comma)
|
||||||
|
. V.toList
|
||||||
|
. coerce
|
||||||
|
|
||||||
|
escape :: Text -> Escaped Text
|
||||||
|
escape t = case Text.find (\c -> c == newline || c == cr || c == comma || c == doubleQuote) t of
|
||||||
|
Nothing -> Escaped t
|
||||||
|
Just _ -> escapeAlways t
|
||||||
|
|
||||||
|
-- | This implementation is definitely suboptimal.
|
||||||
|
-- A better option (which would waste a little space
|
||||||
|
-- but would be much faster) would be to build the
|
||||||
|
-- new text by writing to a buffer directly.
|
||||||
|
escapeAlways :: Text -> Escaped Text
|
||||||
|
escapeAlways t = Escaped $ Text.concat
|
||||||
|
[ textDoubleQuote
|
||||||
|
, Text.replace textDoubleQuote (Text.pack [doubleQuote,doubleQuote]) t
|
||||||
|
, textDoubleQuote
|
||||||
|
]
|
||||||
|
|
||||||
|
-- | Specialized version of 'sepBy1'' which is faster due to not
|
||||||
|
-- accepting an arbitrary separator.
|
||||||
|
sepByDelim1' :: A.Parser a
|
||||||
|
-> Char -- ^ Field delimiter
|
||||||
|
-> A.Parser [a]
|
||||||
|
sepByDelim1' p !delim = liftM2' (:) p loop
|
||||||
|
where
|
||||||
|
loop = do
|
||||||
|
mb <- A.peekChar
|
||||||
|
case mb of
|
||||||
|
Just b | b == delim -> liftM2' (:) (A.anyChar *> p) loop
|
||||||
|
_ -> pure []
|
||||||
|
{-# INLINE sepByDelim1' #-}
|
||||||
|
|
||||||
|
-- | Specialized version of 'sepBy1'' which is faster due to not
|
||||||
|
-- accepting an arbitrary separator.
|
||||||
|
sepByEndOfLine1' :: A.Parser a
|
||||||
|
-> A.Parser [a]
|
||||||
|
sepByEndOfLine1' p = liftM2' (:) p loop
|
||||||
|
where
|
||||||
|
loop = do
|
||||||
|
mb <- A.peekChar
|
||||||
|
case mb of
|
||||||
|
Just b | b == cr ->
|
||||||
|
liftM2' (:) (A.anyChar *> A.char newline *> p) loop
|
||||||
|
| b == newline ->
|
||||||
|
liftM2' (:) (A.anyChar *> p) loop
|
||||||
|
_ -> pure []
|
||||||
|
{-# INLINE sepByEndOfLine1' #-}
|
||||||
|
|
||||||
|
-- | Parse a record, not including the terminating line separator. The
|
||||||
|
-- terminating line separate is not included as the last record in a
|
||||||
|
-- CSV file is allowed to not have a terminating line separator. You
|
||||||
|
-- most likely want to use the 'endOfLine' parser in combination with
|
||||||
|
-- this parser.
|
||||||
|
row :: Char -- ^ Field delimiter
|
||||||
|
-> A.Parser (Vector Text)
|
||||||
|
row !delim = rowNoNewline delim <* endOfLine
|
||||||
|
{-# INLINE row #-}
|
||||||
|
|
||||||
|
rowNoNewline :: Char -- ^ Field delimiter
|
||||||
|
-> A.Parser (Vector Text)
|
||||||
|
rowNoNewline !delim = V.fromList <$!> field delim `sepByDelim1'` delim
|
||||||
|
{-# INLINE rowNoNewline #-}
|
||||||
|
|
||||||
|
-- | Parse a field. The field may be in either the escaped or
|
||||||
|
-- non-escaped format. The return value is unescaped.
|
||||||
|
field :: Char -> A.Parser Text
|
||||||
|
field !delim = do
|
||||||
|
mb <- A.peekChar
|
||||||
|
-- We purposely don't use <|> as we want to commit to the first
|
||||||
|
-- choice if we see a double quote.
|
||||||
|
case mb of
|
||||||
|
Just b | b == doubleQuote -> escapedField
|
||||||
|
_ -> unescapedField delim
|
||||||
|
{-# INLINE field #-}
|
||||||
|
|
||||||
|
escapedField :: A.Parser Text
|
||||||
|
escapedField = do
|
||||||
|
_ <- dquote -- This can probably be replaced with anyChar
|
||||||
|
b <- escapedFieldInner mempty
|
||||||
|
return (LText.toStrict (Builder.toLazyText b))
|
||||||
|
|
||||||
|
escapedFieldInner :: Builder -> A.Parser Builder
|
||||||
|
escapedFieldInner b = do
|
||||||
|
t <- A.takeTill (== doubleQuote)
|
||||||
|
_ <- A.anyChar -- this will always be a double quote
|
||||||
|
c <- A.peekChar'
|
||||||
|
if c == doubleQuote
|
||||||
|
then do
|
||||||
|
_ <- A.anyChar -- this will always be a double quote
|
||||||
|
escapedFieldInner (b `mappend` Builder.fromText t `mappend` Builder.fromText textDoubleQuote)
|
||||||
|
else return (b `mappend` Builder.fromText t)
|
||||||
|
|
||||||
|
unescapedField :: Char -> A.Parser Text
|
||||||
|
unescapedField !delim = A.takeWhile (\ c -> c /= doubleQuote &&
|
||||||
|
c /= newline &&
|
||||||
|
c /= delim &&
|
||||||
|
c /= cr)
|
||||||
|
|
||||||
|
dquote :: A.Parser Char
|
||||||
|
dquote = char doubleQuote
|
||||||
|
|
||||||
|
unescape :: A.Parser Text
|
||||||
|
unescape = (LText.toStrict . Builder.toLazyText) <$!> go mempty where
|
||||||
|
go acc = do
|
||||||
|
h <- A.takeWhile (/= doubleQuote)
|
||||||
|
let rest = do
|
||||||
|
c0 <- A.anyChar
|
||||||
|
c1 <- A.anyChar
|
||||||
|
if (c0 == doubleQuote && c1 == doubleQuote)
|
||||||
|
then go (acc `mappend` Builder.fromText h `mappend` Builder.fromText textDoubleQuote)
|
||||||
|
else fail "invalid CSV escape sequence"
|
||||||
|
done <- A.atEnd
|
||||||
|
if done
|
||||||
|
then return (acc `mappend` Builder.fromText h)
|
||||||
|
else rest
|
||||||
|
|
||||||
|
-- | A strict version of 'Data.Functor.<$>' for monads.
|
||||||
|
(<$!>) :: Monad m => (a -> b) -> m a -> m b
|
||||||
|
f <$!> m = do
|
||||||
|
a <- m
|
||||||
|
return $! f a
|
||||||
|
{-# INLINE (<$!>) #-}
|
||||||
|
|
||||||
|
infixl 4 <$!>
|
||||||
|
|
||||||
|
-- | A version of 'liftM2' that is strict in the result of its first
|
||||||
|
-- action.
|
||||||
|
liftM2' :: (Monad m) => (a -> b -> c) -> m a -> m b -> m c
|
||||||
|
liftM2' f a b = do
|
||||||
|
!x <- a
|
||||||
|
y <- b
|
||||||
|
return (f x y)
|
||||||
|
{-# INLINE liftM2' #-}
|
||||||
|
|
||||||
|
|
||||||
|
-- | Match either a single newline character @\'\\n\'@, or a carriage
|
||||||
|
-- return followed by a newline character @\"\\r\\n\"@, or a single
|
||||||
|
-- carriage return @\'\\r\'@.
|
||||||
|
endOfLine :: A.Parser ()
|
||||||
|
endOfLine = (A.char newline *> return ()) <|> (string (Text.pack "\r\n") *> return ()) <|> (A.char cr *> return ())
|
||||||
|
{-# INLINE endOfLine #-}
|
||||||
|
|
||||||
|
textDoubleQuote :: Text
|
||||||
|
textDoubleQuote = Text.singleton doubleQuote
|
||||||
|
|
||||||
|
doubleQuote, newline, cr, comma :: Char
|
||||||
|
doubleQuote = '\"'
|
||||||
|
newline = '\n'
|
||||||
|
cr = '\r'
|
||||||
|
comma = ','
|
||||||
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
module Siphon.Types where
|
module Siphon.Types where
|
||||||
|
|
||||||
import Data.Vector (Vector)
|
import Data.Vector (Vector)
|
||||||
|
import Colonnade.Types (DecodingRowError)
|
||||||
import qualified Data.Attoparsec.Types as Atto
|
import qualified Data.Attoparsec.Types as Atto
|
||||||
|
|
||||||
newtype Escaped c = Escaped { getEscaped :: c }
|
newtype Escaped c = Escaped { getEscaped :: c }
|
||||||
@ -12,18 +13,26 @@ data Siphon c = Siphon
|
|||||||
, siphonNull :: c -> Bool
|
, siphonNull :: c -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- | This type is provided for convenience with @pipes-text@
|
||||||
|
data CsvResult f c
|
||||||
|
= CsvResultSuccess
|
||||||
|
| CsvResultTextDecodeError
|
||||||
|
| CsvResultDecodeError (DecodingRowError f c)
|
||||||
|
deriving (Show,Read,Eq)
|
||||||
|
|
||||||
|
|
||||||
-- | Consider changing out the use of 'Vector' here
|
-- | Consider changing out the use of 'Vector' here
|
||||||
-- with the humble list instead. It might fuse away
|
-- with the humble list instead. It might fuse away
|
||||||
-- better. Not sure though.
|
-- better. Not sure though.
|
||||||
data SiphonX c1 c2 = SiphonX
|
-- data SiphonX c1 c2 = SiphonX
|
||||||
{ siphonXEscape :: !(c1 -> Escaped c2)
|
-- { siphonXEscape :: !(c1 -> Escaped c2)
|
||||||
, siphonXIntercalate :: !(Vector (Escaped c2) -> c2)
|
-- , siphonXIntercalate :: !(Vector (Escaped c2) -> c2)
|
||||||
}
|
-- }
|
||||||
|
--
|
||||||
data SiphonDecoding c1 c2 = SiphonDecoding
|
-- data SiphonDecoding c1 c2 = SiphonDecoding
|
||||||
{ siphonDecodingParse :: c1 -> Atto.IResult c1 (Vector c2)
|
-- { siphonDecodingParse :: c1 -> Atto.IResult c1 (Vector c2)
|
||||||
, siphonDecodingNull :: c1 -> Bool
|
-- , siphonDecodingNull :: c1 -> Bool
|
||||||
}
|
-- }
|
||||||
|
|
||||||
-- data WithEnd c = WithEnd
|
-- data WithEnd c = WithEnd
|
||||||
-- { withEndEnded :: !Bool
|
-- { withEndEnded :: !Bool
|
||||||
|
|||||||
@ -1,19 +1,23 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
|
||||||
module Main (main) where
|
module Main (main) where
|
||||||
|
|
||||||
import Test.QuickCheck (Gen, Arbitrary(..), choose)
|
import Test.QuickCheck (Gen, Arbitrary(..), choose, elements)
|
||||||
import Test.HUnit (Assertion,(@?=))
|
import Test.HUnit (Assertion,(@?=))
|
||||||
import Test.Framework (defaultMain, testGroup, Test)
|
import Test.Framework (defaultMain, testGroup, Test)
|
||||||
import Test.Framework.Providers.QuickCheck2 (testProperty)
|
import Test.Framework.Providers.QuickCheck2 (testProperty)
|
||||||
import Test.Framework.Providers.HUnit (testCase)
|
import Test.Framework.Providers.HUnit (testCase)
|
||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import GHC.Generics (Generic)
|
||||||
import Data.Either.Combinators
|
import Data.Either.Combinators
|
||||||
import Colonnade.Types
|
import Colonnade.Types
|
||||||
import Siphon.Types
|
import Siphon.Types
|
||||||
import Data.Functor.Identity
|
import Data.Functor.Identity
|
||||||
import Data.Functor.Contravariant (contramap)
|
import Data.Functor.Contravariant (contramap)
|
||||||
import Data.Functor.Contravariant.Divisible (divided,conquered)
|
import Data.Functor.Contravariant.Divisible (divided,conquered)
|
||||||
|
import qualified Data.Text as Text
|
||||||
import qualified Data.ByteString.Builder as Builder
|
import qualified Data.ByteString.Builder as Builder
|
||||||
import qualified Data.ByteString.Lazy as LByteString
|
import qualified Data.ByteString.Lazy as LByteString
|
||||||
import qualified Data.ByteString as ByteString
|
import qualified Data.ByteString as ByteString
|
||||||
@ -22,6 +26,8 @@ import qualified Colonnade.Decoding as Decoding
|
|||||||
import qualified Colonnade.Encoding as Encoding
|
import qualified Colonnade.Encoding as Encoding
|
||||||
import qualified Colonnade.Decoding.ByteString.Char8 as CDB
|
import qualified Colonnade.Decoding.ByteString.Char8 as CDB
|
||||||
import qualified Colonnade.Encoding.ByteString.Char8 as CEB
|
import qualified Colonnade.Encoding.ByteString.Char8 as CEB
|
||||||
|
import qualified Colonnade.Decoding.Text as CDT
|
||||||
|
import qualified Colonnade.Encoding.Text as CET
|
||||||
import qualified Siphon.Encoding as SE
|
import qualified Siphon.Encoding as SE
|
||||||
import qualified Siphon.Decoding as SD
|
import qualified Siphon.Decoding as SD
|
||||||
import qualified Siphon.Content as SC
|
import qualified Siphon.Content as SC
|
||||||
@ -69,8 +75,53 @@ tests =
|
|||||||
>->
|
>->
|
||||||
(void $ SD.headedPipe SC.byteStringChar8 decodingB)
|
(void $ SD.headedPipe SC.byteStringChar8 decodingB)
|
||||||
]
|
]
|
||||||
|
, testGroup "Text encode/decode"
|
||||||
|
[ testCase "Headless Encoding (int,char,bool)"
|
||||||
|
$ runTestScenario
|
||||||
|
SC.text
|
||||||
|
SE.pipe
|
||||||
|
encodingW
|
||||||
|
"4,c,false\n"
|
||||||
|
, testCase "Headless Encoding (Foo,Foo,Foo)"
|
||||||
|
$ runCustomTestScenario
|
||||||
|
SC.text
|
||||||
|
SE.pipe
|
||||||
|
encodingY
|
||||||
|
(FooA,FooA,FooC)
|
||||||
|
"Simple,Simple,\"More\"\"Escaped,\"\"\"\"Chars\"\n"
|
||||||
|
, testProperty "Headless Isomorphism (Foo,Foo,Foo)"
|
||||||
|
$ propIsoPipe $
|
||||||
|
(SE.pipe SC.text encodingY)
|
||||||
|
>->
|
||||||
|
(void $ SD.headlessPipe SC.text decodingY)
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
data Foo = FooA | FooB | FooC
|
||||||
|
deriving (Generic,Eq,Ord,Show,Read,Bounded,Enum)
|
||||||
|
|
||||||
|
instance Arbitrary Foo where
|
||||||
|
arbitrary = elements [minBound..maxBound]
|
||||||
|
|
||||||
|
fooToString :: Foo -> String
|
||||||
|
fooToString x = case x of
|
||||||
|
FooA -> "Simple"
|
||||||
|
FooB -> "With,Escaped\nChars"
|
||||||
|
FooC -> "More\"Escaped,\"\"Chars"
|
||||||
|
|
||||||
|
encodeFoo :: (String -> c) -> Foo -> c
|
||||||
|
encodeFoo f = f . fooToString
|
||||||
|
|
||||||
|
fooFromString :: String -> Either String Foo
|
||||||
|
fooFromString x = case x of
|
||||||
|
"Simple" -> Right FooA
|
||||||
|
"With,Escaped\nChars" -> Right FooB
|
||||||
|
"More\"Escaped,\"\"Chars" -> Right FooC
|
||||||
|
_ -> Left "failed to decode Foo"
|
||||||
|
|
||||||
|
decodeFoo :: (c -> String) -> c -> Either String Foo
|
||||||
|
decodeFoo f = fooFromString . f
|
||||||
|
|
||||||
decodingA :: Decoding Headless ByteString (Int,Char,Bool)
|
decodingA :: Decoding Headless ByteString (Int,Char,Bool)
|
||||||
decodingA = (,,)
|
decodingA = (,,)
|
||||||
<$> Decoding.headless CDB.int
|
<$> Decoding.headless CDB.int
|
||||||
@ -90,6 +141,26 @@ encodingA = contramap tripleToPairs
|
|||||||
$ divided (Encoding.headless CEB.bool)
|
$ divided (Encoding.headless CEB.bool)
|
||||||
$ conquered
|
$ conquered
|
||||||
|
|
||||||
|
encodingW :: Encoding Headless Text (Int,Char,Bool)
|
||||||
|
encodingW = contramap tripleToPairs
|
||||||
|
$ divided (Encoding.headless CET.int)
|
||||||
|
$ divided (Encoding.headless CET.char)
|
||||||
|
$ divided (Encoding.headless CET.bool)
|
||||||
|
$ conquered
|
||||||
|
|
||||||
|
encodingY :: Encoding Headless Text (Foo,Foo,Foo)
|
||||||
|
encodingY = contramap tripleToPairs
|
||||||
|
$ divided (Encoding.headless $ encodeFoo Text.pack)
|
||||||
|
$ divided (Encoding.headless $ encodeFoo Text.pack)
|
||||||
|
$ divided (Encoding.headless $ encodeFoo Text.pack)
|
||||||
|
$ conquered
|
||||||
|
|
||||||
|
decodingY :: Decoding Headless Text (Foo,Foo,Foo)
|
||||||
|
decodingY = (,,)
|
||||||
|
<$> Decoding.headless (decodeFoo Text.unpack)
|
||||||
|
<*> Decoding.headless (decodeFoo Text.unpack)
|
||||||
|
<*> Decoding.headless (decodeFoo Text.unpack)
|
||||||
|
|
||||||
encodingB :: Encoding Headed ByteString (Int,Char,Bool)
|
encodingB :: Encoding Headed ByteString (Int,Char,Bool)
|
||||||
encodingB = contramap tripleToPairs
|
encodingB = contramap tripleToPairs
|
||||||
$ divided (Encoding.headed "number" CEB.int)
|
$ divided (Encoding.headed "number" CEB.int)
|
||||||
@ -120,6 +191,18 @@ runTestScenario s p e c =
|
|||||||
Pipes.yield (4,'c',False) >-> p s e
|
Pipes.yield (4,'c',False) >-> p s e
|
||||||
) @?= c
|
) @?= c
|
||||||
|
|
||||||
|
runCustomTestScenario :: (Monoid c, Eq c, Show c)
|
||||||
|
=> Siphon c
|
||||||
|
-> (Siphon c -> Encoding f c a -> Pipe a c Identity ())
|
||||||
|
-> Encoding f c a
|
||||||
|
-> a
|
||||||
|
-> c
|
||||||
|
-> Assertion
|
||||||
|
runCustomTestScenario s p e a c =
|
||||||
|
( mconcat $ Pipes.toList $
|
||||||
|
Pipes.yield a >-> p s e
|
||||||
|
) @?= c
|
||||||
|
|
||||||
-- testEncodingA :: Assertion
|
-- testEncodingA :: Assertion
|
||||||
-- testEncodingA = runTestScenario encodingA "4,c,false\n"
|
-- testEncodingA = runTestScenario encodingA "4,c,false\n"
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,7 @@ packages:
|
|||||||
- 'colonnade'
|
- 'colonnade'
|
||||||
- 'reflex-dom-colonnade'
|
- 'reflex-dom-colonnade'
|
||||||
- 'siphon'
|
- 'siphon'
|
||||||
|
- 'geolite-csv'
|
||||||
# Dependency packages to be pulled from upstream that are not in the resolver
|
# Dependency packages to be pulled from upstream that are not in the resolver
|
||||||
# (e.g., acme-missiles-0.3)
|
# (e.g., acme-missiles-0.3)
|
||||||
extra-deps:
|
extra-deps:
|
||||||
@ -48,6 +49,7 @@ extra-deps:
|
|||||||
- 'aeson-0.9.0.1'
|
- 'aeson-0.9.0.1'
|
||||||
- 'haskell-src-exts-1.16.0.1'
|
- 'haskell-src-exts-1.16.0.1'
|
||||||
- 'syb-0.5.1'
|
- 'syb-0.5.1'
|
||||||
|
- 'ip-0.8.4'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user