sv 0.1 → 1.0
raw patch · 35 files changed
+504/−3734 lines, 35 filesdep +Onlydep +cassavadep +hw-dsvdep −ansi-wl-pprintdep −charsetdep −containersdep ~contravariantdep ~hedgehogdep ~tasty
Dependencies added: Only, cassava, hw-dsv, sv-core
Dependencies removed: ansi-wl-pprint, charset, containers, deepseq, mtl, parsec, profunctors, readable, void
Dependency ranges changed: contravariant, hedgehog, tasty, tasty-hedgehog, trifecta
Files
- changelog.md +5/−0
- src/Data/Sv.hs +48/−16
- src/Data/Sv/Alien/Cassava.hs +114/−0
- src/Data/Sv/Decode.hs +3/−563
- src/Data/Sv/Decode/Error.hs +0/−99
- src/Data/Sv/Decode/Type.hs +0/−146
- src/Data/Sv/Encode.hs +2/−423
- src/Data/Sv/Encode/Options.hs +0/−98
- src/Data/Sv/Encode/Type.hs +0/−48
- src/Data/Sv/Parse.hs +30/−93
- src/Data/Sv/Parse/Internal.hs +0/−170
- src/Data/Sv/Parse/Options.hs +0/−97
- src/Data/Sv/Print.hs +0/−104
- src/Data/Sv/Print/Internal.hs +0/−73
- src/Data/Sv/Print/Options.hs +0/−80
- src/Data/Sv/Structure.hs +14/−0
- src/Data/Sv/Syntax.hs +0/−18
- src/Data/Sv/Syntax/Field.hs +0/−116
- src/Data/Sv/Syntax/Record.hs +0/−176
- src/Data/Sv/Syntax/Sv.hs +0/−219
- src/Data/Vector/NonEmpty.hs +0/−79
- src/Text/Escape.hs +0/−124
- src/Text/Newline.hs +0/−84
- src/Text/Quote.hs +0/−72
- src/Text/Space.hs +0/−190
- sv.cabal +43/−62
- test/Data/Sv/CassavaTest.hs +49/−0
- test/Data/Sv/DecodeTest.hs +57/−21
- test/Data/Sv/EncodeTest.hs +5/−5
- test/Data/Sv/Generators.hs +0/−115
- test/Data/Sv/ParseTest.hs +0/−215
- test/Data/Sv/PrintTest.hs +0/−32
- test/Data/Sv/RoundTrips.hs +0/−190
- test/Data/Sv/RoundTripsDecodeEncode.hs +130/−0
- test/tasty.hs +4/−6
changelog.md view
@@ -1,5 +1,10 @@ # Revision history for sv +## 1.0 -- 2018-07-19+* Split the package into sv, sv-core, svfactor. This removes Data.Sv.{Parse,Print,Syntax}+ and changes types in Data.Sv.Decode and Data.Sv.Encode.+ Compatibility with version 0.1 is minimal+ ## 0.1 -- 2018-03-06 * First version. Released on an unsuspecting world.
src/Data/Sv.hs view
@@ -19,57 +19,89 @@ module Data.Sv ( -- * Decoding- decode- , parseDecode- , parseDecode'+ parseDecode , parseDecodeFromFile- , parseDecodeFromFile'+ , parseDecodeFromDsvCursor+ , decode , decodeMay , decodeEither , decodeEither' , (>>==) , (==<<)+ , module Data.Sv.Parse , module Data.Sv.Decode.Type , module Data.Sv.Decode.Error - -- * Parsing- , module Data.Sv.Parse-- -- * Printing- , module Data.Sv.Print- -- * Encoding , encode , encodeToFile , encodeToHandle , encodeBuilder , encodeRow- , encodeSv , module Data.Sv.Encode.Type , module Data.Sv.Encode.Options - -- * Core data types- , module Data.Sv.Syntax+ -- * Structure+ , module Data.Sv.Structure - -- * Re-exports from contravariant and semigroupoids+ -- * Re-exports from contravariant, validation, and semigroupoids , Alt (..) , Contravariant (..) , Divisible (..) , divided , Decidable (..) , chosen+ , Validation (..) ) where +import Control.Monad.IO.Class (MonadIO (liftIO))+import qualified Data.Attoparsec.ByteString.Lazy as AL+import Data.Bifunctor (Bifunctor (first))+import Data.ByteString (ByteString)+import qualified Data.ByteString.UTF8 as UTF8+import qualified Data.ByteString.Lazy as LBS+import HaskellWorks.Data.Dsv.Lazy.Cursor as DSV import Data.Functor.Alt (Alt (..)) import Data.Functor.Contravariant (Contravariant (..)) import Data.Functor.Contravariant.Divisible (Divisible (..), divided, Decidable (..), chosen)+import Data.Validation (Validation (..)) +import Data.Sv.Alien.Cassava (field) import Data.Sv.Decode import Data.Sv.Decode.Type import Data.Sv.Decode.Error import Data.Sv.Encode import Data.Sv.Encode.Options import Data.Sv.Encode.Type+import Data.Sv.Structure import Data.Sv.Parse-import Data.Sv.Print-import Data.Sv.Syntax++-- | Parse a 'ByteString' as an Sv, and then decode it with the given decoder.+parseDecode ::+ Decode' ByteString a+ -> ParseOptions+ -> LBS.ByteString+ -> DecodeValidation ByteString [a]+parseDecode d opts bs =+ let sep = _separator opts+ cursor = DSV.makeCursor sep bs+ in parseDecodeFromDsvCursor d opts cursor++-- | Load a file, parse it, and decode it.+parseDecodeFromFile ::+ MonadIO m+ => Decode' ByteString a+ -> ParseOptions+ -> FilePath+ -> m (DecodeValidation ByteString [a])+parseDecodeFromFile d opts fp =+ parseDecode d opts <$> liftIO (LBS.readFile fp)++-- | Decode from a 'DsvCursor'+parseDecodeFromDsvCursor :: Decode' ByteString a -> ParseOptions -> DsvCursor -> DecodeValidation ByteString [a]+parseDecodeFromDsvCursor d opts cursor =+ let toField = Prelude.either badParse pure . parse+ parse = first UTF8.fromString . AL.eitherResult . AL.parse field+ in flip bindValidation (decode d) . traverse (traverse toField) . DSV.toListVector $ case _headedness opts of+ Unheaded -> cursor+ Headed -> nextPosition (nextRow cursor)
+ src/Data/Sv/Alien/Cassava.hs view
@@ -0,0 +1,114 @@+{-+Copyright (c)2012, Johan Tibell++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 Johan Tibell 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.+-}++module Data.Sv.Alien.Cassava (+ field+) where++import Data.ByteString.Builder (byteString, toLazyByteString, charUtf8)+import Data.Attoparsec.ByteString.Char8 (char)+import qualified Data.Attoparsec.ByteString as A+import qualified Data.Attoparsec.Lazy as AL+import qualified Data.Attoparsec.Zepto as Z+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Data.ByteString.Lazy (toStrict)+import qualified Data.ByteString.Unsafe as BS+import GHC.Word (Word8)++type Field = ByteString++doubleQuote, newline, cr :: Word8+doubleQuote = 34+newline = 10+cr = 13++-- | 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 <$!>++-- | Parse a field. The field may be in either the escaped or+-- non-escaped format. The return value is unescaped.+field :: AL.Parser Field+field = do+ _ <- A.skipWhile (== 32) -- space+ mb <- A.peekWord8+ -- 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+{-# INLINE field #-}++escapedField :: AL.Parser ByteString+escapedField = do+ _ <- dquote+ -- The scan state is 'True' if the previous character was a double+ -- quote. We need to drop a trailing double quote left by scan.+ s <- BS.init <$> A.scan False (\s c -> if c == doubleQuote+ then Just (not s)+ else if s then Nothing+ else Just False)+ if doubleQuote `BS.elem` s+ then case Z.parse unescape s of+ Right r -> return r+ Left err -> fail err+ else return s++unescapedField :: AL.Parser ByteString+unescapedField = A.takeWhile (\ c -> c /= doubleQuote &&+ c /= newline &&+ c /= cr)++dquote :: AL.Parser Char+dquote = char '"'++unescape :: Z.Parser ByteString+unescape = (toStrict . toLazyByteString) <$!> go mempty where+ go acc = do+ h <- Z.takeWhile (/= doubleQuote)+ let rest = do+ start <- Z.take 2+ if (BS.unsafeHead start == doubleQuote &&+ BS.unsafeIndex start 1 == doubleQuote)+ then go (acc `mappend` byteString h `mappend` charUtf8 '"')+ else fail "invalid CSV escape sequence"+ done <- Z.atEnd+ if done+ then return (acc `mappend` byteString h)+ else rest
src/Data/Sv/Decode.hs view
@@ -1,7 +1,3 @@-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}- {-| Module : Data.Sv.Decode Copyright : (C) CSIRO 2017-2018@@ -11,7 +7,7 @@ Portability : non-portable This module contains data structures, combinators, and primitives for-decoding an 'Sv' into a list of your Haskell datatype.+decoding a CSV into a list of your Haskell datatype. A file can be read with 'parseDecodeFromFile'. If you already have the text data in memory, it can be decoded with 'parseDecode'.@@ -33,563 +29,7 @@ -} module Data.Sv.Decode (- -- * The types- Decode (..)-, Decode'-, Validation (..)-, DecodeValidation-, DecodeError (..)-, DecodeErrors (..)-- -- * Running Decodes-, decode-, parseDecode-, parseDecode'-, parseDecodeFromFile-, parseDecodeFromFile'---- * Convenience constructors and functions-, decodeMay-, decodeEither-, decodeEither'-, mapErrors-, alterInput---- * Primitive Decodes--- ** Field-based-, contents-, untrimmed-, raw-, char-, byteString-, utf8-, lazyUtf8-, lazyByteString-, string-, int-, integer-, float-, double-, boolean-, boolean'-, ignore-, replace-, exactly-, emptyField--- ** Row-based-, row-, rowWithSpacing---- * Combinators-, choice-, element-, optionalField-, ignoreFailure-, orEmpty-, either-, orElse-, orElseE-, categorical-, categorical'-, (>>==)-, (==<<)-, bindDecode---- * Building Decodes from Readable-, decodeRead-, decodeRead'-, decodeReadWithMsg---- * Building Decodes from parsers-, withTrifecta-, withAttoparsec-, withParsec---- * Working with errors-, onError-, decodeError-, unexpectedEndOfRow-, expectedEndOfRow-, unknownCategoricalValue-, badParse-, badDecode-, validateEither-, validateEither'-, validateMaybe-, validateMaybe'---- * Implementation details-, runDecode-, buildDecode-, mkDecode-, mkDecodeWithQuotes-, mkDecodeWithSpaces-, promote+ module Data.Sv.Decode.Core ) where -import Prelude hiding (either)-import qualified Prelude--import Control.Lens (alaf, view)-import Control.Monad.IO.Class (MonadIO)-import Control.Monad.Reader (ReaderT (ReaderT))-import Control.Monad.State (state)-import Data.Attoparsec.ByteString (parseOnly)-import qualified Data.Attoparsec.ByteString as A (Parser)-import Data.Bifunctor (first, second)-import Data.ByteString (ByteString)-import qualified Data.ByteString.UTF8 as UTF8-import qualified Data.ByteString.Lazy as LBS-import Data.Char (toUpper)-import Data.Foldable (toList)-import Data.Functor.Alt (Alt ((<!>)))-import Data.Functor.Compose (Compose (Compose, getCompose))-import Data.List.NonEmpty (NonEmpty ((:|)))-import Data.Monoid (First (First))-import Data.Profunctor (lmap)-import Data.Readable (Readable (fromBS))-import Data.Semigroup (Semigroup ((<>)), sconcat)-import Data.Semigroup.Foldable (asum1)-import Data.Set (Set, fromList, member)-import Data.String (IsString (fromString))-import Data.Text (Text)-import Data.Text.Encoding (decodeUtf8')-import qualified Data.Text.Lazy as LT-import Data.Validation (_Validation)-import Data.Vector (Vector, (!))-import qualified Data.Vector as V-import Text.Parsec (Parsec)-import qualified Text.Parsec as P (parse)-import qualified Text.Trifecta as Tri--import Data.Sv.Decode.Error-import Data.Sv.Decode.Type-import qualified Data.Sv.Parse as P-import Data.Sv.Parse.Options (ParseOptions)-import Data.Sv.Syntax.Field (Field (Unquoted, Quoted), fieldContents, SpacedField, Spaced (Spaced))-import Data.Sv.Syntax.Record (Record, _fields)-import Data.Sv.Syntax.Sv (Sv, recordList)-import Text.Space (HorizontalSpace, Spaced (_value), spacedValue)---- | Decodes a sv into a list of its values using the provided 'Decode'-decode :: Decode' s a -> Sv s -> DecodeValidation s [a]-decode f = traverse (promote f) . recordList---- | Parse a 'ByteString' as an Sv, and then decode it with the given decoder.------ This version uses 'Text.Trifecta.Trifecta' to parse the 'ByteString', which is assumed to--- be UTF-8 encoded. If you want a different library, use 'parseDecode''.-parseDecode ::- Decode' ByteString a- -> ParseOptions ByteString- -> ByteString- -> DecodeValidation ByteString [a]-parseDecode = parseDecode' P.trifecta---- | Parse text as an Sv, and then decode it with the given decoder.------ This version lets you choose which parsing library to use by providing an--- 'P.SvParser'. Common selections are 'P.trifecta' and 'P.attoparsecByteString'.-parseDecode' ::- P.SvParser s- -> Decode' s a- -> ParseOptions s- -> s- -> DecodeValidation s [a]-parseDecode' svp d opts s =- Prelude.either badDecode pure (P.parseSv' svp opts s) `bindValidation` decode d---- | Load a file, parse it, and decode it.------ This version uses Trifecta to parse the file, which is assumed to be UTF-8--- encoded.-parseDecodeFromFile ::- MonadIO m- => Decode' ByteString a- -> ParseOptions ByteString- -> FilePath- -> m (DecodeValidation ByteString [a])-parseDecodeFromFile = parseDecodeFromFile' P.trifecta---- | Load a file, parse it, and decode it.------ This version lets you choose which parsing library to use by providing an--- 'P.SvParser'. Common selections are 'P.trifecta' and 'P.attoparsecByteString'.-parseDecodeFromFile' ::- MonadIO m- => P.SvParser s- -> Decode' s a- -> ParseOptions s- -> FilePath- -> m (DecodeValidation s [a])-parseDecodeFromFile' svp d opts fp = do- sv <- P.parseSvFromFile' svp opts fp- pure (Prelude.either badDecode pure sv `bindValidation` decode d)---- | Build a 'Decode', given a function that returns 'Maybe'.------ Return the given error if the function returns 'Nothing'.-decodeMay :: DecodeError e -> (s -> Maybe a) -> Decode e s a-decodeMay e f = mkDecode (validateMaybe e . f)---- | Build a 'Decode', given a function that returns 'Either'.-decodeEither :: (s -> Either (DecodeError e) a) -> Decode e s a-decodeEither f = mkDecode (validateEither . f)---- | Build a 'Decode', given a function that returns 'Either', and a function to--- build the error.-decodeEither' :: (e -> DecodeError e') -> (s -> Either e a) -> Decode e' s a-decodeEither' e f = mkDecode (validateEither' e . f)---- | Succeeds with the whole field structure, including spacing and quoting information-raw :: Decode e s (SpacedField s)-raw = mkDecodeWithSpaces pure---- | Returns the field contents. This keeps the spacing around an unquoted field.-untrimmed :: Monoid s => (HorizontalSpace -> s) -> Decode e s s-untrimmed fromSpace =- let sp = foldMap fromSpace- spaceIfNecessary (Spaced b a f) = case f of- Unquoted s -> mconcat [sp b, s, sp a]- Quoted _ _ -> view fieldContents f- in fmap spaceIfNecessary raw---- | Get the contents of a field without doing any decoding. This never fails.-contents :: Decode e s s-contents = mkDecode pure---- | Grab the whole row as a 'Vector'-row :: Decode e s (Vector s)-row = (fmap . fmap) (view (spacedValue.fieldContents)) rowWithSpacing---- | Grab the whole row, including all spacing and quoting information,--- as a 'Vector'-rowWithSpacing :: Decode e s (Vector (SpacedField s))-rowWithSpacing =- Decode . Compose . DecodeState . ReaderT $ \v ->- state (const (pure v, Ind (V.length v)))---- | Get a field that's a single char. This will fail if there are mulitple--- characters in the field.-char :: Decode' ByteString Char-char = string >>== \cs -> case cs of- [] -> badDecode "Expected single char but got empty string"- (c:[]) -> pure c- (_:_:_) -> badDecode ("Expected single char but got " <> UTF8.fromString cs)---- | Get the contents of a field as a bytestring.------ Alias for 'contents'-byteString :: Decode' ByteString ByteString-byteString = contents---- | Get the contents of a UTF-8 encoded field as 'Text'------ This will also work for ASCII text, as ASCII is a subset of UTF-8-utf8 :: Decode' ByteString Text-utf8 = contents >>==- Prelude.either (badDecode . UTF8.fromString . show) pure . decodeUtf8'---- | Get the contents of a field as a lazy 'Data.Text.Lazy.Text'-lazyUtf8 :: Decode' ByteString LT.Text-lazyUtf8 = LT.fromStrict <$> utf8---- | Get the contents of a field as a lazy 'Data.ByteString.Lazy.ByteString'-lazyByteString :: Decode' ByteString LBS.ByteString-lazyByteString = LBS.fromStrict <$> contents---- | Get the contents of a field as a 'String'-string :: Decode' ByteString String-string = UTF8.toString <$> contents---- | Throw away the contents of a field. This is useful for skipping unneeded fields.-ignore :: Decode e s ()-ignore = replace ()---- | Throw away the contents of a field, and return the given value.-replace :: a -> Decode e s a-replace a = a <$ contents---- | Decode exactly the given string, or else fail.-exactly :: (Semigroup s, Eq s, IsString s) => s -> Decode' s s-exactly s = contents >>== \z ->- if s == z- then pure s- else badDecode (sconcat ("'":|[z,"' was not equal to '",s,"'"]))---- | Decode a UTF-8 'ByteString' field as an 'Int'-int :: Decode' ByteString Int-int = named "int"---- | Decode a UTF-8 'ByteString' field as an 'Integer'-integer :: Decode' ByteString Integer-integer = named "integer"---- | Decode a UTF-8 'ByteString' field as a 'Float'-float :: Decode' ByteString Float-float = named "float"---- | Decode a UTF-8 'ByteString' field as a 'Double'-double :: Decode' ByteString Double-double = named "double"---- | Decode a field as a 'Bool'------ This aims to be tolerant to different forms a boolean might take.-boolean :: (IsString s, Ord s) => Decode' s Bool-boolean = boolean' fromString---- | Decode a field as a 'Bool'. This version lets you provide the fromString--- function that's right for you, since 'Data.String.IsString' on a--- 'Data.ByteString.ByteString' will do the wrong thing in the case of many--- encodings such as UTF-16 or UTF-32.------ This aims to be tolerant to different forms a boolean might take.-boolean' :: Ord s => (String -> s) -> Decode' s Bool-boolean' s =- categorical' [- (False, fmap s ["false", "False", "FALSE", "f", "F", "0", "n", "N", "no", "No", "NO", "off", "Off", "OFF"])- , (True, fmap s ["true", "True", "TRUE", "t", "T", "1", "y", "Y", "yes", "Yes", "YES", "on", "On", "ON"])- ]---- | Succeed only when the given field is the empty string.------ The empty string surrounded in quotes or spaces is still the empty string.-emptyField :: (Eq s, IsString s, Semigroup s) => Decode' s ()-emptyField = contents >>== \c ->- if c == fromString "" then- pure ()- else- badDecode ("Expected emptiness but got: " <> c)---- | Choose the leftmost 'Decode' that succeeds. Alias for '<!>'-choice :: Decode e s a -> Decode e s a -> Decode e s a-choice = (<!>)---- | Choose the leftmost 'Decode' that succeeds. Alias for 'asum1'-element :: NonEmpty (Decode e s a) -> Decode e s a-element = asum1---- | Try the given 'Decode'. If it fails, instead succeed with 'Nothing'.-ignoreFailure :: Decode e s a -> Decode e s (Maybe a)-ignoreFailure a = Just <$> a <!> Nothing <$ ignore---- | If the field is the empty string, succeed with 'Nothing'.--- Otherwise try the given 'Decode'.-orEmpty :: (Eq s, IsString s, Semigroup s) => Decode' s a -> Decode' s (Maybe a)-orEmpty a = Nothing <$ emptyField <!> Just <$> a---- | Try the given 'Decode'. If it fails, succeed without consuming anything.------ This usually isn't what you want. 'ignoreFailure' and 'orEmpty' are more--- likely what you are after.-optionalField :: Decode e s a -> Decode e s (Maybe a)-optionalField a = Just <$> a <!> pure Nothing---- | Try the first, then try the second, and wrap the winner in an 'Either'.------ This is left-biased, meaning if they both succeed, left wins.-either :: Decode e s a -> Decode e s b -> Decode e s (Either a b)-either a b = fmap Left a <!> fmap Right b---- | Try the given decoder, otherwise succeed with the given value.-orElse :: Decode e s a -> a -> Decode e s a-orElse f a = f <!> replace a---- | Try the given decoder, or if it fails succeed with the given value, in an 'Either'.-orElseE :: Decode e s b -> a -> Decode e s (Either a b)-orElseE b a = fmap Right b <!> replace (Left a)---- | Decode categorical data, given a list of the values and the strings which match them.------ Usually this is used with sum types with nullary constructors.------ > data TrafficLight = Red | Amber | Green--- > categorical [(Red, "red"), (Amber, "amber"), (Green, "green")]-categorical :: (Ord s, Show a) => [(a, s)] -> Decode' s a-categorical = categorical' . fmap (fmap pure)---- | Decode categorical data, given a list of the values and lists of strings--- which match them.------ This version allows for multiple strings to match each value, which is--- useful for when the categories are inconsistently labelled.------ > data TrafficLight = Red | Amber | Green--- > categorical' [(Red, ["red", "R"]), (Amber, ["amber", "orange", "A"]), (Green, ["green", "G"])]------ For another example of its usage, see the source for 'boolean'.-categorical' :: forall s a . (Ord s, Show a) => [(a, [s])] -> Decode' s a-categorical' as =- let as' :: [(a, Set s)]- as' = fmap (second fromList) as- go :: s -> (a, Set s) -> Maybe a- go s (a, set) =- if s `member` set- then Just a- else Nothing- in contents >>== \s ->- validateMaybe (UnknownCategoricalValue s (fmap snd as)) $- alaf First foldMap (go s) as'---- | Use the 'Readable' instance to try to decode the given value.-decodeRead :: Readable a => Decode' ByteString a-decodeRead = decodeReadWithMsg (mappend "Couldn't parse ")---- | Use the 'Readable' instance to try to decode the given value,--- or fail with the given error message.-decodeRead' :: Readable a => ByteString -> Decode' ByteString a-decodeRead' e = decodeReadWithMsg (const e)---- | Use the 'Readable' instance to try to decode the given value,--- or use the value to build an error message.-decodeReadWithMsg :: Readable a => (ByteString -> e) -> Decode e ByteString a-decodeReadWithMsg e = contents >>== \c ->- maybe (badDecode (e c)) pure . fromBS $ c---- | Given the name of a type, try to decode it using 'Readable', -named :: Readable a => ByteString -> Decode' ByteString a-named name =- let vs' = ['a','e','i','o','u']- vs = fmap toUpper vs' ++ vs'- n c = if c `elem` vs then "n" else ""- n' = foldMap (n . fst) . UTF8.uncons- n'' = n' name- space = " "- in decodeReadWithMsg $ \bs ->- mconcat ["Couldn't parse \"", bs, "\" as a", n'', space, name]---- | Map over the errors of a 'Decode'------ To map over the other two parameters, use the 'Data.Profunctor.Profunctor' instance.-mapErrors :: (e -> x) -> Decode e s a -> Decode x s a-mapErrors f (Decode (Compose r)) = Decode (Compose (fmap (first (fmap f)) r))---- | This transforms a @Decode' s a@ into a @Decode' t a@. It needs--- functions in both directions because the errors can include fragments of the--- input.------ @alterInput :: (s -> t) -> (t -> s) -> Decode' s a -> Decode' t a@-alterInput :: (e -> x) -> (t -> s) -> Decode e s a -> Decode x t a-alterInput f g = mapErrors f . lmap g------ Promoting parsers to 'Decode's---- | Build a 'Decode' from a Trifecta parser-withTrifecta :: Tri.Parser a -> Decode' ByteString a-withTrifecta =- mkParserFunction- (validateTrifectaResult (BadDecode . UTF8.fromString))- (flip Tri.parseByteString mempty)---- | Build a 'Decode' from an Attoparsec parser-withAttoparsec :: A.Parser a -> Decode' ByteString a-withAttoparsec =- mkParserFunction- (validateEither' (BadDecode . fromString))- parseOnly---- | Build a 'Decode' from a Parsec parser-withParsec :: Parsec ByteString () a -> Decode' ByteString a-withParsec =- -- Parsec will include a position, but it will only confuse the user- -- since it won't correspond obviously to a position in their source file.- let dropPos = drop 1 . dropWhile (/= ':')- in mkParserFunction- (validateEither' (BadDecode . UTF8.fromString . dropPos . show))- (\p s -> P.parse p mempty s)--mkParserFunction ::- Tri.CharParsing p- => (f a -> DecodeValidation ByteString a)- -> (p a -> ByteString -> f a)- -> p a- -> Decode' ByteString a-mkParserFunction err run p =- let p' = p <* Tri.eof- in byteString >>== (err . run p')-{-# INLINE mkParserFunction #-}---- | Convenience to get the underlying function out of a Decode in a useful form-runDecode :: Decode e s a -> Vector (SpacedField s) -> Ind -> (DecodeValidation e a, Ind)-runDecode = runDecodeState . getCompose . unwrapDecode-{-# INLINE runDecode #-}---- | This can be used to build a 'Decode' whose value depends on the--- result of another 'Decode'. This is especially useful since 'Decode' is not--- a 'Monad'.------ If you need something like this but with more power, look at 'bindDecode'-(>>==) :: Decode e s a -> (a -> DecodeValidation e b) -> Decode e s b-(>>==) = flip (==<<)-infixl 1 >>==-{-# INLINE (>>==) #-}---- | flipped '>>=='-(==<<) :: (a -> DecodeValidation e b) -> Decode e s a -> Decode e s b-(==<<) f (Decode c) =- Decode (rmapC (`bindValidation` (view _Validation . f)) c)- where- rmapC g (Compose fga) = Compose (fmap g fga)-infixr 1 ==<<---- | Bind through a 'Decode'.------ This bind does not agree with the 'Applicative' instance because it does--- not accumulate multiple error values. This is a violation of the 'Monad'--- laws, meaning 'Decode' is not a 'Monad'.------ That is not to say that there is anything wrong with using this function.--- It can be quite useful.-bindDecode :: Decode e s a -> (a -> Decode e s b) -> Decode e s b-bindDecode d f =- buildDecode $ \v i ->- case runDecode d v i of- (Failure e, i') -> (Failure e, i')- (Success a, i') -> runDecode (f a) v i'---- | Run a 'Decode', and based on its errors build a new 'Decode'.-onError :: Decode e s a -> (DecodeErrors e -> Decode e s a) -> Decode e s a-onError d f =- buildDecode $ \v i ->- case runDecode d v i of- (Failure e, i') -> runDecode (f e) v i'- (Success a, i') -> (Success a, i')---- | Build a 'Decode' from a function.------ This version gives you just the contents of the field, with no information--- about the spacing or quoting around that field.-mkDecode :: (s -> DecodeValidation e a) -> Decode e s a-mkDecode f = mkDecodeWithQuotes (f . view fieldContents)---- | Build a 'Decode' from a function.------ This version gives you access to the whole 'Field', which includes--- information about whether quotes were used, and if so which ones.-mkDecodeWithQuotes :: (Field s -> DecodeValidation e a) -> Decode e s a-mkDecodeWithQuotes f = mkDecodeWithSpaces (f . _value)---- | Build a 'Decode' from a function.------ This version gives you access to the whole 'SpacedField', which includes--- information about spacing both before and after the field, and about quotes--- if they were used.-mkDecodeWithSpaces :: (SpacedField s -> DecodeValidation e a) -> Decode e s a-mkDecodeWithSpaces f =- Decode . Compose . DecodeState . ReaderT $ \v -> state $ \(Ind i) ->- if i >= length v- then (unexpectedEndOfRow, Ind i)- else (f (v ! i), Ind (i+1))---- | promotes a Decode to work on a whole 'Record' at once. This does not need--- to be called by the user. Instead use 'decode'.-promote :: Decode' s a -> Record s -> DecodeValidation s a-promote dec rs =- let vec = V.fromList . toList . _fields $ rs- len = length vec- in case runDecode dec vec (Ind 0) of- (d, Ind i) ->- if i >= len- then d- else d *> expectedEndOfRow (V.force (V.drop i vec))+import Data.Sv.Decode.Core
− src/Data/Sv/Decode/Error.hs
@@ -1,99 +0,0 @@-{-|-Module : Data.Sv.Decode.Error-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--}--module Data.Sv.Decode.Error (- DecodeError (..)-, DecodeErrors (..)---- * Convenience constructors-, decodeError-, unexpectedEndOfRow-, expectedEndOfRow-, unknownCategoricalValue-, badParse-, badDecode---- * Conversions-, validateEither-, validateEither'-, validateMaybe-, validateMaybe'-, trifectaResultToEither-, validateTrifectaResult---- * Re-exports from @validation@-, bindValidation-) where--import Data.Validation (Validation (Failure), bindValidation)-import Data.Vector (Vector)-import qualified Text.Trifecta as Trifecta (Result (Success, Failure), _errDoc)--import Data.Sv.Decode.Type-import Data.Sv.Syntax.Field---- | Build a failing 'DecodeValidation'-decodeError :: DecodeError e -> DecodeValidation e a-decodeError = Failure . DecodeErrors . pure---- | Fail with 'UnexpectedEndOfRow'-unexpectedEndOfRow :: DecodeValidation e a-unexpectedEndOfRow = decodeError UnexpectedEndOfRow---- | Fail with 'ExpectedEndOfRow'. This takes the rest of the row, so that it--- can be displayed to the user.-expectedEndOfRow :: Vector (SpacedField e) -> DecodeValidation e a-expectedEndOfRow = decodeError . ExpectedEndOfRow---- | Fail with 'UnknownCategoricalValue'.--- It takes the unknown value and the list of good categorical values.------ This mostly exists to be used by the 'Data.Sv.Decode.categorical' function.-unknownCategoricalValue :: e -> [[e]] -> DecodeValidation e a-unknownCategoricalValue unknown valids =- decodeError (UnknownCategoricalValue unknown valids)---- | Fail with 'BadParse' with the given message. This is for when the parse--- step fails, and decoding does not even begin.-badParse :: e -> DecodeValidation e a-badParse = decodeError . BadParse---- | Fail with 'BadDecode' with the given message. This is something of a--- generic error for when decoding a field goes wrong.-badDecode :: e -> DecodeValidation e a-badDecode = decodeError . BadDecode---- | Build a 'DecodeValidation' from an 'Either'-validateEither :: Either (DecodeError e) a -> DecodeValidation e a-validateEither = validateEither' id---- | Build a 'DecodeValidation' from an 'Either', given a function to build the error.-validateEither' :: (e -> DecodeError e') -> Either e a -> DecodeValidation e' a-validateEither' f = either (decodeError . f) pure---- | Build a 'DecodeValidation' from a 'Maybe'. You have to supply an error--- to use in the 'Nothing' case-validateMaybe :: DecodeError e -> Maybe b -> DecodeValidation e b-validateMaybe e = maybe (decodeError e) pure---- | Build a 'DecodeValidation' from a function that returns a 'Maybe'--- You have to supply an error to use in the 'Nothing' case-validateMaybe' :: (a -> Maybe b) -> DecodeError e -> a -> DecodeValidation e b-validateMaybe' ab e a = validateMaybe e (ab a)---- | Helper to convert "Text.Trifecta" 'Text.Trifecta.Result' to 'Either'.-trifectaResultToEither :: Trifecta.Result a -> Either String a-trifectaResultToEither result = case result of- Trifecta.Success a -> Right a- Trifecta.Failure e -> Left . show . Trifecta._errDoc $ e---- | Convert a "Text.Trifecta" 'Text.Trifecta.Result' to a 'DecodeValidation'-validateTrifectaResult :: (String -> DecodeError e) -> Trifecta.Result a -> DecodeValidation e a-validateTrifectaResult f = validateEither' f . trifectaResultToEither-
− src/Data/Sv/Decode/Type.hs
@@ -1,146 +0,0 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE TupleSections #-}--{-|-Module : Data.Sv.Decode.Type-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--}--module Data.Sv.Decode.Type (- Decode (..)-, Decode'-, buildDecode-, DecodeState (..)-, runDecodeState-, Ind (..)-, DecodeError (..)-, DecodeErrors (..)-, DecodeValidation-, Validation (..)-) where--import Control.DeepSeq (NFData)-import Control.Monad.Reader (ReaderT (ReaderT, runReaderT), MonadReader, withReaderT)-import Control.Monad.State (State, runState, state, MonadState)-import Data.Functor.Alt (Alt ((<!>)))-import Data.Functor.Apply (Apply)-import Data.Functor.Bind (Bind ((>>-)))-import Data.Functor.Compose (Compose (Compose))-import Data.List.NonEmpty-import Data.Semigroup-import Data.Profunctor (Profunctor (lmap, rmap))-import Data.Validation (Validation (Success, Failure))-import Data.Vector (Vector)-import GHC.Generics (Generic)--import Data.Sv.Syntax.Field (SpacedField)---- | A 'Decode e s a' is for decoding some fields from a CSV row into our type 'a'.------ The second type parameter (@s@) is the input string type--- (usually 'ByteString' or 'Text').--- The first type parameter (@e@) is the type of strings which occur in errors.--- Under most circumstances you want these type paraters to coincide, but they--- don't have to. They are two separate type parameters instead of one so that--- 'Decode' can have a 'Data.Profunctor.Profunctor' instance.------ There are primitive 'Decode's, and combinators for composing or--- otherwise manipulating them. In particular, 'Decode' is an--- 'Applicative' functor and an 'Alt' from the semigroupoids package.------ 'Decode' is not a 'Monad', but we can perform monad-like operations on--- it with 'Data.Sv.Decode.Field.>>==' and 'Data.Sv.Decode.bindDecode'-newtype Decode e s a =- Decode { unwrapDecode :: Compose (DecodeState s) (DecodeValidation e) a }- deriving (Functor, Apply, Applicative)---- | 'Decode'' is 'Decode' with the input and error types the same. You usually--- want them to be the same, and most primitives are set up this way.-type Decode' s = Decode s s--instance Alt (Decode e s) where- Decode (Compose as) <!> Decode (Compose bs) =- buildDecode $ \v i ->- case runDecodeState as v i of- (a, j) -> case runDecodeState bs v i of- (b, k) ->- let a' = fmap (,j) a- b' = fmap (,k) b- in case a' <!> b' of- Failure e -> (Failure e, k)- Success (z, m) -> (Success z, m)--instance Profunctor (Decode e) where- lmap f (Decode (Compose dec)) = Decode (Compose (lmap f dec))- rmap = fmap---- | As we decode a row of data, we walk through its 'Data.Sv.Syntax.Field's. This 'Monad'--- keeps track of our remaining 'Data.Sv.Syntax.Field's.-newtype DecodeState s a =- DecodeState { getDecodeState :: ReaderT (Vector (SpacedField s)) (State Ind) a }- deriving (Functor, Apply, Applicative, Monad, MonadReader (Vector (SpacedField s)), MonadState Ind)--instance Bind (DecodeState s) where- (>>-) = (>>=)--instance Profunctor DecodeState where- lmap f (DecodeState s) = DecodeState (withReaderT (fmap (fmap (fmap f))) s)- rmap = fmap---- | Convenient constructor for 'Decode' that handles all the newtype noise for you.-buildDecode :: (Vector (SpacedField s) -> Ind -> (DecodeValidation e a, Ind)) -> Decode e s a-buildDecode f = Decode . Compose . DecodeState . ReaderT $ \v -> state $ \i -> f v i---- | Convenient function to run a DecodeState-runDecodeState :: DecodeState s a -> Vector (SpacedField s) -> Ind -> (a, Ind)-runDecodeState = fmap runState . runReaderT . getDecodeState---- | Newtype for indices into the field vector-newtype Ind = Ind Int---- | 'DecodeError' is a value indicating what went wrong during a parse or--- decode. Its constructor indictates the type of error which occured, and--- there is usually an associated string with more finely-grained details.-data DecodeError e =- -- | I was looking for another field, but I am at the end of the row- UnexpectedEndOfRow- -- | I should be at the end of the row, but I found extra fields- | ExpectedEndOfRow (Vector (SpacedField e))- -- | This decoder was built using the 'categorical' primitive for categorical data- | UnknownCategoricalValue e [[e]]- -- | The parser failed, meaning decoding proper didn't even begin- | BadParse e- -- | Some other kind of decoding failure occured- | BadDecode e- deriving (Eq, Ord, Show, Generic)--instance Functor DecodeError where- fmap f d = case d of- UnexpectedEndOfRow -> UnexpectedEndOfRow- ExpectedEndOfRow v -> ExpectedEndOfRow (fmap (fmap (fmap f)) v)- UnknownCategoricalValue e ess -> UnknownCategoricalValue (f e) (fmap (fmap f) ess)- BadParse e -> BadParse (f e)- BadDecode e -> BadDecode (f e)--instance NFData e => NFData (DecodeError e)---- | 'DecodeErrors' is a 'Semigroup' full of 'DecodeError'. It is used as the--- error side of a 'DecodeValidation'. When multiple errors occur, they will--- be collected.-newtype DecodeErrors e =- DecodeErrors (NonEmpty (DecodeError e))- deriving (Eq, Ord, Show, Semigroup, Generic)--instance Functor DecodeErrors where- fmap f (DecodeErrors nel) = DecodeErrors (fmap (fmap f) nel)--instance NFData e => NFData (DecodeErrors e)---- | 'DecodeValidation' is the error-accumulating 'Applicative' underlying--- 'Decode'-type DecodeValidation e = Validation (DecodeErrors e)
src/Data/Sv/Encode.hs view
@@ -1,6 +1,3 @@-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE OverloadedStrings #-}- {-| Module : Data.Sv.Encode Copyright : (C) CSIRO 2017-2018@@ -8,428 +5,10 @@ Maintainer : George Wilson <george.wilson@data61.csiro.au> Stability : experimental Portability : non-portable--This module is intended to be imported qualified as follows--@import Data.Sv.Encode as E@--To produce a CSV file from data types, build an 'Encode' for your data-type. This module contains primitives, combinators, and type class instances-to help you to do so.--'Encode' is a 'Contravariant' functor, as well as a 'Divisible' and-'Decidable'. 'Divisible' is the contravariant form of 'Applicative',-while 'Decidable' is the contravariant form of 'Control.Applicative.Alternative'.-These type classes will provide useful combinators for working with 'Encode's.--Specialised to 'Encode', the function 'Data.Functor.Contravariant.Divisible.divide'-from 'Divisible' has the type:--@-divide :: (a -> (b,c)) -> Encode b -> Encode c -> Encode a-@--which can be read "if 'a' can be split into 'b' and 'c', and I can handle-'b', and I can handle 'c', then I can handle 'a'".--Here the "I can handle"-part corresponds to the 'Encode'. If we think of (covariant) functors as-being "full of" 'a', then we can think of contravariant functors as being-"able to handle" 'a'.--How does it work? Perform the split on the 'a', handle the 'b' by converting-it into some text,-handle the 'c' by also converting it to some text, then put each of those-text fragments into their own field in the CSV.--Similarly, the function 'Data.Functor.Contravariant.Divisible.choose'-from 'Decidable', specialsed to 'Encode', has the type:--@-choose :: (a -> Either b c) -> Encode b -> Encode c -> Encode a-@--which can be read "if 'a' is either 'b' or 'c', and I can handle 'b',-and I can handle 'c', then I can handle 'a'".--This works by performing the split, then checking whether 'b' or 'c' resulted,-then using the appropriate 'Encode'.--For an example of encoding, see-<https://github.com/qfpl/sv/blob/master/examples/src/Data/Sv/Example/Encoding.hs Encoding.hs> -} module Data.Sv.Encode (- Encode (..)---- * Convenience constructors-, mkEncodeBS-, mkEncodeWithOpts-, unsafeBuilder---- * Running an Encode-, encode-, encodeToHandle-, encodeToFile-, encodeBuilder-, encodeRow-, encodeRowBuilder-, encodeSv---- * Options-, module Data.Sv.Encode.Options---- * Primitive encodes--- ** Field-based-, const-, show-, nop-, empty-, orEmpty-, char-, int-, integer-, float-, double-, boolTrueFalse-, booltruefalse-, boolyesno-, boolYesNo-, boolYN-, bool10-, string-, text-, byteString-, lazyByteString--- ** Row-based-, row---- * Combinators-, (?>)-, (<?)-, (?>>)-, (<<?)-, encodeOf-, encodeOfMay---- * Unsafe encodes-, unsafeString-, unsafeText-, unsafeByteString-, unsafeLazyByteString-, unsafeByteStringBuilder-, unsafeConst+ module Data.Sv.Encode.Core ) where -import qualified Prelude as P-import Prelude hiding (const, show)--import Control.Applicative ((<$>), (<**>))-import Control.Lens (Getting, preview, review, view)-import Control.Monad (join)-import qualified Data.Bool as B (bool)-import qualified Data.ByteString as Strict-import qualified Data.ByteString.Builder as BS-import qualified Data.ByteString.Lazy as LBS-import Data.Foldable (fold, foldMap, toList)-import Data.Functor.Contravariant (Contravariant (contramap))-import Data.Functor.Contravariant.Divisible (Divisible (conquer), Decidable (choose))-import Data.List.NonEmpty (NonEmpty, nonEmpty)-import Data.Monoid (Monoid (mempty), First, (<>), mconcat)-import Data.Sequence (Seq, ViewL (EmptyL, (:<)), viewl, (<|))-import qualified Data.Sequence as Seq-import qualified Data.Sequence as S (singleton, empty)-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import System.IO (BufferMode (BlockBuffering), Handle, hClose, hSetBinaryMode, hSetBuffering, openFile, IOMode (WriteMode))--import Data.Sv.Encode.Options (EncodeOptions (..), HasEncodeOptions (..), HasSeparator (..), defaultEncodeOptions)-import Data.Sv.Encode.Type (Encode (Encode, getEncode))-import Data.Sv.Syntax.Field (Field (Unquoted), SpacedField, unescapedField)-import Data.Sv.Syntax.Record (Record (Record), Records (EmptyRecords), emptyRecord, mkRecords, recordNel)-import Data.Sv.Syntax.Sv (Sv (Sv), Header (Header))-import qualified Data.Vector.NonEmpty as V-import Text.Escape (Escaper, Escaper', Unescaped (Unescaped), escapeChar, escapeString, escapeText, escapeUtf8, escapeUtf8Lazy)-import Text.Newline (newlineToString)-import Text.Space (Spaced (Spaced), spacesString)-import Text.Quote (quoteChar)---- | Make an 'Encode' from a function that builds one 'Field'.-mkEncodeBS :: (a -> LBS.ByteString) -> Encode a-mkEncodeBS = unsafeBuilder . fmap BS.lazyByteString---- | Make an 'Encode' from a function that builds one 'Field'.-mkEncodeWithOpts :: (EncodeOptions -> a -> BS.Builder) -> Encode a-mkEncodeWithOpts = Encode . fmap (fmap pure)---- | Make an encode from any function that returns a ByteString 'Builder'.-unsafeBuilder :: (a -> BS.Builder) -> Encode a-unsafeBuilder b = Encode (\_ a -> pure (b a))-{-# INLINE unsafeBuilder #-}---- | Encode the given list with the given 'Encode', configured by the given--- 'EncodeOptions'.-encode :: Encode a -> EncodeOptions -> [a] -> LBS.ByteString-encode enc opts = BS.toLazyByteString . encodeBuilder enc opts---- | Encode, writing the output to a file handle.-encodeToHandle :: Encode a -> EncodeOptions -> [a] -> Handle -> IO ()-encodeToHandle enc opts as h =- BS.hPutBuilder h (encodeBuilder enc opts as)---- | Encode, writing to a file. This is way is more efficient than encoding to--- a 'ByteString' and then writing to file.-encodeToFile :: Encode a -> EncodeOptions -> [a] -> FilePath -> IO ()-encodeToFile enc opts as fp = do- h <- openFile fp WriteMode- hSetBuffering h (BlockBuffering Nothing)- hSetBinaryMode h True- encodeToHandle enc opts as h- hClose h---- | Encode to a ByteString 'Builder', which is useful if you are going--- to combine the output with other 'ByteString's.-encodeBuilder :: Encode a -> EncodeOptions -> [a] -> BS.Builder-encodeBuilder e opts as =- let enc = encodeRowBuilder e opts- nl = newlineToString (_newline opts)- terminal = if _terminalNewline opts then nl else mempty- in case as of- [] -> terminal- (a:as') -> enc a <> mconcat [nl <> enc a' | a' <- as'] <> terminal---- | Encode one row only-encodeRow :: Encode a -> EncodeOptions -> a -> LBS.ByteString-encodeRow e opts = BS.toLazyByteString . encodeRowBuilder e opts---- | Encode one row only, as a ByteString 'Builder'-encodeRowBuilder :: Encode a -> EncodeOptions -> a -> BS.Builder-encodeRowBuilder e opts =- let addSeparators = intersperseSeq (BS.charUtf8 (view separator opts))- quotep = foldMap (BS.charUtf8 . review quoteChar) (view quote opts)- addQuotes x = quotep <> x <> quotep- mkSpaces optic = BS.stringUtf8 . review spacesString . view optic $ opts- bspaces = mkSpaces spacingBefore- aspaces = mkSpaces spacingAfter- addSpaces x = bspaces <> x <> aspaces- in fold . addSeparators . fmap (addSpaces . addQuotes) . getEncode e opts---- | Build an 'Sv' rather than going straight to 'ByteString'. This allows you--- to query the Sv or run sanity checks.-encodeSv :: Encode a -> EncodeOptions -> Maybe (NonEmpty Strict.ByteString) -> [a] -> Sv Strict.ByteString-encodeSv e opts headerStrings as =- let encoded :: [Seq BS.Builder]- encoded = getEncode e opts <$> as- nl = view newline opts- sep = view separator opts- mkSpaced = Spaced (_spacingBefore opts) (_spacingAfter opts)- mkField = maybe Unquoted unescapedField (_quote opts)- mkHeader r = Header r nl- mkRecord :: NonEmpty z -> Record z- mkRecord = recordNel . fmap (mkSpaced . mkField)- header :: Maybe (Header Strict.ByteString)- header = mkHeader . mkRecord <$> headerStrings- rs :: Records Strict.ByteString- rs = l2rs (b2r <$> encoded)- l2rs = maybe EmptyRecords (mkRecords nl) . nonEmpty -- Records . fmap (skrinple nl) . nonEmpty- terminal = if _terminalNewline opts then [nl] else []- b2f :: BS.Builder -> SpacedField Strict.ByteString- b2f = mkSpaced . mkField . LBS.toStrict . BS.toLazyByteString- b2r :: Seq BS.Builder -> Record Strict.ByteString- b2r = maybe emptyRecord (Record . V.fromNel) . nonEmpty . toList . fmap b2f- in Sv sep header rs terminal---- | Encode this 'Data.ByteString.ByteString' every time, ignoring the input.-const :: Strict.ByteString -> Encode a-const b = contramap (pure b) byteString---- | Build an 'Encode' using a type's 'Show' instance.-show :: Show a => Encode a-show = contramap P.show string---- | Don't encode anything.-nop :: Encode a-nop = conquer---- | Encode anything as the empty string.-empty :: Encode a-empty = Encode (pure (pure (pure mempty)))---- | Lift an Encode to be able to hanlde 'Maybe', by using the empty string--- in the case of 'Nothing'-orEmpty :: Encode a -> Encode (Maybe a)-orEmpty = choose (maybe (Left ()) Right) empty---- | Build an 'Encode' for 'Maybe' given a 'Just' and a 'Nothing' encode.-(?>) :: Encode a -> Encode () -> Encode (Maybe a)-(?>) = flip (<?)-{-# INLINE (?>) #-}---- | Build an 'Encode' for 'Maybe' given a 'Nothing' and a 'Just' encode.-(<?) :: Encode () -> Encode a -> Encode (Maybe a)-(<?) = choose (maybe (Left ()) Right)-{-# INLINE (<?) #-}---- | Build an 'Encode' for 'Maybe' given a 'Just' encode and a--- 'Data.ByteString.Strict.ByteString' for the 'Nothing' case.-(?>>) :: Encode a -> Strict.ByteString -> Encode (Maybe a)-(?>>) a s = a ?> const s-{-# INLINE (?>>) #-}---- | Build an 'Encode' for 'Maybe' given a 'Data.ByteString.Strict.ByteString'--- for the 'Nothing' case and a 'Just' encode.-(<<?) :: Strict.ByteString -> Encode a -> Encode (Maybe a)-(<<?) = flip (?>>)-{-# INLINE (<<?) #-}---- | Encode a list as a whole row at once, using the same 'Encode'--- for every element-row :: Encode s -> Encode [s]-row enc = Encode $ \opts list -> join $ Seq.fromList $ fmap (getEncode enc opts) list---- | Encode a single 'Char'-char :: Encode Char-char = escaped escapeChar BS.charUtf8 BS.stringUtf8---- | Encode an 'Int'-int :: Encode Int-int = unsafeBuilder BS.intDec---- | Encode an 'Integer'-integer :: Encode Integer-integer = unsafeBuilder BS.integerDec---- | Encode a 'Float'-float :: Encode Float-float = unsafeBuilder BS.floatDec---- | Encode a 'Double'-double :: Encode Double-double = unsafeBuilder BS.doubleDec---- | Encode a 'String'-string :: Encode String-string = escaped' escapeString BS.stringUtf8---- | Encode a 'Data.Text.Text'-text :: Encode T.Text-text = escaped' escapeText (BS.byteString . T.encodeUtf8)---- | Encode a strict 'Data.ByteString.ByteString'-byteString :: Encode Strict.ByteString-byteString = escaped' escapeUtf8 BS.byteString---- | Encode a lazy 'Data.ByteString.Lazy.ByteString'-lazyByteString :: Encode LBS.ByteString-lazyByteString = escaped' escapeUtf8Lazy BS.lazyByteString--escaped :: Escaper s t -> (s -> BS.Builder) -> (t -> BS.Builder) -> Encode s-escaped esc sb tb = mkEncodeWithOpts $ \opts s ->- case _quote opts of- Nothing -> sb s- Just q -> tb $ esc (review quoteChar q) (Unescaped s)--escaped' :: Escaper' s -> (s -> BS.Builder) -> Encode s-escaped' escaper = join (escaped escaper)---- | Encode a 'Bool' as False or True-boolTrueFalse :: Encode Bool-boolTrueFalse = mkEncodeBS $ B.bool "False" "True"---- | Encode a 'Bool' as false or true-booltruefalse :: Encode Bool-booltruefalse = mkEncodeBS $ B.bool "false" "true"---- | Encode a 'Bool' as no or yes-boolyesno :: Encode Bool-boolyesno = mkEncodeBS $ B.bool "no" "yes"---- | Encode a 'Bool' as No or Yes-boolYesNo :: Encode Bool-boolYesNo = mkEncodeBS $ B.bool "No" "Yes"---- | Encode a 'Bool' as N or Y-boolYN :: Encode Bool-boolYN = mkEncodeBS $ B.bool "N" "Y"---- | Encode a 'Bool' as 0 or 1-bool10 :: Encode Bool-bool10 = mkEncodeBS $ B.bool "0" "1"---- | Given an optic from @s@ to @a@, Try to use it to build an encode.------ @--- encodeOf :: Iso' s a -> Encode a -> Encode s--- encodeOf :: Lens' s a -> Encode a -> Encode s--- encodeOf :: Prism' s a -> Encode a -> Encode s--- encodeOf :: Traversal' s a -> Encode a -> Encode s--- encodeOf :: Fold s a -> Encode a -> Encode s--- encodeOf :: Getter s a -> Encode a -> Encode s--- @------ This is very useful when you have a prism for each constructor of your type.--- You can define an 'Encode' as follows:------ @--- myEitherEncode :: Encode a -> Encode b -> Encode (Either a b)--- myEitherEncode encA encB = encodeOf _Left encA <> encodeOf _Right encB--- @------ In this example, when the prism lookup returns 'Nothing', the empty encoder--- is returned. This is the 'mempty' for the 'Encode' monoid, so it won't--- add a field to the resulting CSV. This is the behaviour you want for--- combining a collection of prisms.------ But this encoder also works with lenses (or weaker optics), which will--- never fail their lookup, in which case it never returns 'mempty'.--- So this actually does the right thing for both sum and product types.-encodeOf :: Getting (First a) s a -> Encode a -> Encode s-encodeOf g = encodeOfMay g . choose (maybe (Left ()) Right) conquer---- | Like 'encodeOf', but you can handle 'Nothing' however you'd like.--- In 'encodeOf', it is handled by the Encode which does nothing,--- but for example you might like to use 'orEmpty' to encode an empty field.-encodeOfMay :: Getting (First a) s a -> Encode (Maybe a) -> Encode s-encodeOfMay g x = contramap (preview g) x---- | Encode a 'String' really quickly.--- If the string has quotes in it, they will not be escaped properly, so--- the result maybe not be valid CSV-unsafeString :: Encode String-unsafeString = unsafeBuilder BS.stringUtf8---- | Encode 'Data.Text.Text' really quickly.--- If the text has quotes in it, they will not be escaped properly, so--- the result maybe not be valid CSV-unsafeText :: Encode T.Text-unsafeText = unsafeBuilder (BS.byteString . T.encodeUtf8)---- | Encode ByteString 'Data.ByteString.Builder.Builder' really quickly.--- If the builder builds a string with quotes in it, they will not be escaped--- properly, so the result maybe not be valid CSV-unsafeByteStringBuilder :: Encode BS.Builder-unsafeByteStringBuilder = unsafeBuilder id---- | Encode a 'Data.ByteString.ByteString' really quickly.--- If the string has quotes in it, they will not be escaped properly, so--- the result maybe not be valid CSV-unsafeByteString :: Encode Strict.ByteString-unsafeByteString = unsafeBuilder BS.byteString---- | Encode a 'Data.ByteString.Lazy.ByteString' really quickly.--- If the string has quotes in it, they will not be escaped properly, so--- the result maybe not be valid CSV-unsafeLazyByteString :: Encode LBS.ByteString-unsafeLazyByteString = unsafeBuilder BS.lazyByteString---- | Encode this 'Data.ByteString.ByteString' really quickly every time, ignoring the input.--- If the string has quotes in it, they will not be escaped properly, so--- the result maybe not be valid CSV-unsafeConst :: Strict.ByteString -> Encode a-unsafeConst b = contramap (pure b) unsafeByteString---- Added in containers 0.5.8, but we duplicate it here to support older GHCs-intersperseSeq :: a -> Seq a -> Seq a-intersperseSeq y xs = case viewl xs of- EmptyL -> S.empty- p :< ps -> p <| (ps <**> (pure y <| S.singleton id))+import Data.Sv.Encode.Core
− src/Data/Sv/Encode/Options.hs
@@ -1,98 +0,0 @@-{-|-Module : Data.Sv.Encode.Options-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--Options to configure encoding--}--module Data.Sv.Encode.Options (- EncodeOptions (EncodeOptions, _encodeSeparator, _spacingBefore, _spacingAfter, _quote, _newline, _terminalNewline)-, HasEncodeOptions (encodeOptions, spacingBefore, spacingAfter, quote, newline, terminalNewline)-, HasSeparator (..)-, defaultEncodeOptions-) where--import Control.Lens (Lens')--import Data.Sv.Syntax.Sv (Separator, HasSeparator (separator), comma)-import Text.Newline (Newline (CRLF))-import Text.Space (Spaces)-import Text.Quote (Quote (DoubleQuote))---- | These are options to configure encoding. A default is provided as--- 'defaultEncodeOptions'.-data EncodeOptions =- EncodeOptions {- -- | Are your values separated by commas, tabs, or something else? Default: comma- _encodeSeparator :: Separator- -- | Between a comma and the next value, would you like some spacing? Default: no spacing- , _spacingBefore :: Spaces- -- | Between a value and the next comma, would you like some spacing? Default: no spacing- , _spacingAfter :: Spaces- -- | Would you like quotes around your values? If so, double quotes or single? Deafult: Double quotes- , _quote :: Maybe Quote- -- | What kind of newline would you like? Default: CRLF- , _newline :: Newline- -- | Should the file be terminated with a newline? Default: No- , _terminalNewline :: Bool- }---- | Classy lenses for 'EncodeOptions'------ @--- import Control.Lens------ defaultEncodeOptinons & quote .~ Just DoubleQuote & newline .~ LF--- @-class HasSeparator c => HasEncodeOptions c where- encodeOptions :: Lens' c EncodeOptions- newline :: Lens' c Newline- {-# INLINE newline #-}- quote :: Lens' c (Maybe Quote)- {-# INLINE quote #-}- spacingAfter :: Lens' c Spaces- {-# INLINE spacingAfter #-}- spacingBefore :: Lens' c Spaces- {-# INLINE spacingBefore #-}- terminalNewline :: Lens' c Bool- {-# INLINE terminalNewline #-}- newline = encodeOptions . newline- quote = encodeOptions . quote- spacingAfter = encodeOptions . spacingAfter- spacingBefore = encodeOptions . spacingBefore- terminalNewline = encodeOptions . terminalNewline--instance HasSeparator EncodeOptions where- separator f (EncodeOptions x1 x2 x3 x4 x5 x6) =- fmap (\ y -> EncodeOptions y x2 x3 x4 x5 x6) (f x1)- {-# INLINE separator #-}--instance HasEncodeOptions EncodeOptions where- encodeOptions = id- {-# INLINE encodeOptions #-}- newline f (EncodeOptions x1 x2 x3 x4 x5 x6) =- fmap (\ y -> EncodeOptions x1 x2 x3 x4 y x6) (f x5)- {-# INLINE newline #-}- quote f (EncodeOptions x1 x2 x3 x4 x5 x6) =- fmap (\ y -> EncodeOptions x1 x2 x3 y x5 x6) (f x4)- {-# INLINE quote #-}- spacingAfter f (EncodeOptions x1 x2 x3 x4 x5 x6) =- fmap (\ y -> EncodeOptions x1 x2 y x4 x5 x6) (f x3)- {-# INLINE spacingAfter #-}- spacingBefore f (EncodeOptions x1 x2 x3 x4 x5 x6) =- fmap (\ y -> EncodeOptions x1 y x3 x4 x5 x6) (f x2)- {-# INLINE spacingBefore #-}- terminalNewline f (EncodeOptions x1 x2 x3 x4 x5 x6) =- fmap (\ y -> EncodeOptions x1 x2 x3 x4 x5 y) (f x6)- {-# INLINE terminalNewline #-}---- | The default options for encoding.------ The default is a CSV file with double-quotes, CRLF lines, no spacing around--- fields, and no terminating newline.-defaultEncodeOptions :: EncodeOptions-defaultEncodeOptions = EncodeOptions comma mempty mempty (Just DoubleQuote) CRLF False
− src/Data/Sv/Encode/Type.hs
@@ -1,48 +0,0 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}--{-|-Module : Data.Sv.Encode.Type-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--The core type for encoding--}--module Data.Sv.Encode.Type (- Encode (Encode, getEncode)-) where--import Data.Bifoldable (bifoldMap)-import qualified Data.ByteString.Builder as BS-import Data.Functor.Contravariant (Contravariant (contramap))-import Data.Functor.Contravariant.Divisible (Divisible (divide, conquer), Decidable (choose, lose))-import Data.Semigroup (Semigroup)-import Data.Sequence (Seq)-import Data.Void (absurd)--import Data.Sv.Encode.Options---- | An 'Encode' converts its argument into one or more textual fields, to be--- written out as CSV.------ It is 'Semigroup', Contravariant', 'Divisible', and 'Decidable', allowing--- for composition of these values to build bigger 'Encode's from smaller ones.-newtype Encode a =- Encode { getEncode :: EncodeOptions -> a -> Seq BS.Builder }- deriving (Semigroup, Monoid)--instance Contravariant Encode where- contramap f (Encode g) = Encode $ fmap (. f) g--instance Divisible Encode where- conquer = Encode mempty- divide f (Encode x) (Encode y) =- Encode $ \e a -> bifoldMap (x e) (y e) (f a)--instance Decidable Encode where- lose f = Encode (const (absurd . f))- choose f (Encode x) (Encode y) =- Encode $ \e a -> either (x e) (y e) (f a)
src/Data/Sv/Parse.hs view
@@ -8,107 +8,44 @@ -} module Data.Sv.Parse (- parseSv-, parseSv'-, parseSvFromFile-, parseSvFromFile'-, separatedValues--, module Data.Sv.Parse.Options-, SvParser (..)-, trifecta-, attoparsecByteString-, attoparsecText+ ParseOptions (..)+, defaultParseOptions+, defaultSeparator+, defaultHeadedness ) where -import Control.Lens (view)-import Control.Monad.IO.Class (MonadIO, liftIO)-import qualified Data.Attoparsec.ByteString as BS (parseOnly)-import qualified Data.Attoparsec.Text as Text (parseOnly)-import Data.Bifunctor (first)-import Data.ByteString (ByteString)-import qualified Data.ByteString as BS-import Data.Text (Text)-import qualified Data.Text.IO as Text-import qualified Text.Trifecta as Tri--import Data.Sv.Syntax.Sv (Sv)-import Data.Sv.Decode.Error (trifectaResultToEither)-import Data.Sv.Parse.Internal (separatedValues)-import Data.Sv.Parse.Options+import Data.Sv.Structure.Separator (Separator, HasSeparator (separator), comma)+import Data.Sv.Structure.Headedness (Headedness (Headed), HasHeadedness (headedness)) --- | Parse a 'ByteString' as an Sv.+-- | A 'ParseOptions' informs the parser how to parse your file. ----- This version uses Trifecta, hence it assumes its input is UTF-8 encoded.-parseSv :: ParseOptions ByteString -> ByteString -> Either ByteString (Sv ByteString)-parseSv = parseSv' trifecta+-- A default is provided as 'defaultParseOptions', seen below.+data ParseOptions =+ ParseOptions {+ -- | Which separator does the file use? Usually this is 'comma', but it can+ -- also be 'pipe', or any other 'Word8' ('Separator' = 'Word8')+ _separator :: Separator --- | Parse some text as an Sv.------ This version lets you choose which parsing library to use by providing an--- 'SvParser'. Common selections are 'trifecta' and 'attoparsecByteString'.-parseSv' :: SvParser s -> ParseOptions s -> s -> Either s (Sv s)-parseSv' svp opts s =- let enc = view encodeString opts- in first enc $ runSvParser svp opts s+ -- | Whether there is a header row with column names or not.+ , _headedness :: Headedness+ } --- | Load a file and parse it as an 'Sv'.------ This version uses Trifecta, hence it assumes its input is UTF-8 encoded.-parseSvFromFile :: MonadIO m => ParseOptions ByteString -> FilePath -> m (Either ByteString (Sv ByteString))-parseSvFromFile = parseSvFromFile' trifecta+instance HasHeadedness ParseOptions where+ headedness f (ParseOptions s h) = ParseOptions s <$> f h --- | Load a file and parse it as an 'Sv'.------ This version lets you choose which parsing library to use by providing an--- 'SvParser'. Common selections are 'trifecta' and 'attoparsecByteString'.-parseSvFromFile' :: MonadIO m => SvParser s -> ParseOptions s -> FilePath -> m (Either s (Sv s))-parseSvFromFile' svp opts fp =- let enc = view encodeString opts- in liftIO (first enc <$> runSvParserFromFile svp opts fp)+instance HasSeparator ParseOptions where+ separator f (ParseOptions s h) = (\y -> ParseOptions y h) <$> f s --- | Which parsing library should be used to parse the document?------ The parser is written in terms of the @parsers@ library, meaning it can be--- instantiated to several different parsing libraries. By default, we use--- 'trifecta', because "Text.Trifecta"s error messages are so helpful.--- 'attoparsecByteString' is faster though, if your input is ASCII and- -- you care a lot about speed.+-- | Default parsing options. ----- It is worth noting that Trifecta assumes UTF-8 encoding of the input data.--- UTF-8 is backwards-compatible with 7-bit ASCII, so this will work for many--- documents. However, not all documents are ASCII or UTF-8. For example, our--- <https://github.com/qfpl/sv/blob/master/examples/csv/species.csv species.csv>--- test file is Windows-1252, which is a non-ISO extension--- of latin1 8-bit ASCII. For documents encoded as Windows-1252, Trifecta's--- assumption is invalid and parse errors result.--- 'Attoparsec' works fine for this character encoding, but it wouldn't work--- well on a UTF-8 encoded document including non-ASCII characters.-data SvParser s = SvParser- { runSvParser :: ParseOptions s -> s -> Either String (Sv s)- , runSvParserFromFile :: ParseOptions s -> FilePath -> IO (Either String (Sv s))- }---- | An 'SvParser' that uses "Text.Trifecta". Trifecta assumes its input is UTF-8, and--- provides helpful clang-style error messages.-trifecta :: SvParser ByteString-trifecta = SvParser- { runSvParser = \opts bs -> trifectaResultToEither $ Tri.parseByteString (separatedValues opts) mempty bs- , runSvParserFromFile = \opts fp -> trifectaResultToEither <$> Tri.parseFromFileEx (separatedValues opts) fp- }+-- The default is a comma separator, with a header at the top of the file.+defaultParseOptions :: ParseOptions+defaultParseOptions = ParseOptions defaultSeparator defaultHeadedness --- | An 'SvParser' that uses "Data.Attoparsec.ByteString". This is the fastest--- provided 'SvParser', but it has poorer error messages.-attoparsecByteString :: SvParser ByteString-attoparsecByteString = SvParser- { runSvParser = \opts bs -> BS.parseOnly (separatedValues opts) bs- , runSvParserFromFile = \opts fp -> BS.parseOnly (separatedValues opts) <$> BS.readFile fp- }+-- | The default separator is comma.+defaultSeparator :: Separator+defaultSeparator = comma --- | An 'SvParser' that uses "Data.Attoparsec.Text". This is helpful if--- your input is in the form of 'Text'.-attoparsecText :: SvParser Text-attoparsecText = SvParser- { runSvParser = \opts t -> Text.parseOnly (separatedValues opts) t- , runSvParserFromFile = \opts fp -> Text.parseOnly (separatedValues opts) <$> Text.readFile fp- }+-- | The default is that a header is present.+defaultHeadedness :: Headedness+defaultHeadedness = Headed
− src/Data/Sv/Parse/Internal.hs
@@ -1,170 +0,0 @@-{-|-Module : Data.Sv.Parse.Internal-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--This module contains internal implementation details of sv's parser.-As the Internal name suggests, this file is exempt from the PVP. Depend-on this module at your own risk!--}--module Data.Sv.Parse.Internal (- separatedValues- , header- , field- , singleQuotedField- , doubleQuotedField- , unquotedField- , spaced- , spacedField- , record- , records- , ending-) where--import Control.Applicative (Alternative ((<|>), empty), optional)-import Control.Lens (review, view)-import Data.CharSet (CharSet, (\\))-import qualified Data.CharSet as CharSet (fromList, insert, singleton)-import Data.Functor (($>), (<$>), void)-import Data.List.NonEmpty (NonEmpty ((:|)))-import Data.Maybe (fromMaybe)-import Data.Semigroup ((<>))-import qualified Data.Vector as V-import Text.Parser.Char (CharParsing, char, notChar, noneOfSet, oneOfSet, string)-import Text.Parser.Combinators (between, choice, eof, many, notFollowedBy, sepEndBy, try)--import Data.Sv.Syntax.Sv (Sv (Sv), Header, mkHeader, noHeader, Headedness (Unheaded, Headed), headedness, Separator)-import Data.Sv.Syntax.Field (Field (Unquoted, Quoted))-import Data.Sv.Syntax.Record (Record (Record), Records (Records, EmptyRecords))-import Data.Sv.Parse.Options (ParseOptions, separator, endOnBlankLine, encodeString)-import Data.Vector.NonEmpty as V-import Text.Escape (Unescaped (Unescaped))-import Text.Newline (Newline (CR, CRLF, LF))-import Text.Space (HorizontalSpace (Space, Tab), Spaces, Spaced, betwixt)-import Text.Quote (Quote (SingleQuote, DoubleQuote), quoteChar)---- | This function is in newer versions of the parsers package, but in--- order to maintain compatibility with older versions I've left it here.-sepEndByNonEmpty :: Alternative m => m a -> m sep -> m (NonEmpty a)-sepEndByNonEmpty p sep = (:|) <$> p <*> ((sep *> sepEndBy p sep) <|> pure [])---- | Parse a field surrounded by single quotes-singleQuotedField :: CharParsing m => (String -> s) -> m (Field s)-singleQuotedField = quotedField SingleQuote---- | Parse a field surrounded by double quotes-doubleQuotedField :: CharParsing m => (String -> s) -> m (Field s)-doubleQuotedField = quotedField DoubleQuote---- | Given a quote, parse its escaped form (which is it repeated twice)-escapeQuote :: CharParsing m => Quote -> m Char-escapeQuote q =- let c = review quoteChar q- in try (string (two c)) $> c--two :: a -> [a]-two a = [a,a]--quotedField :: CharParsing m => Quote -> (String -> s) -> m (Field s)-quotedField quote str =- let q = review quoteChar quote- c = char q- cc = escapeQuote quote- in Quoted quote . Unescaped . str <$> between c c (many (cc <|> notChar q))---- | Parse a field that is not surrounded by quotes-unquotedField :: CharParsing m => Separator -> (String -> s) -> m (Field s)-unquotedField sep str =- let spaceSet = CharSet.fromList " \t" \\ CharSet.singleton sep- oneSpace = oneOfSet spaceSet- nonSpaceFieldChar = noneOfSet (newlineOr sep <> spaceSet)- terminalWhitespace = many oneSpace *> fieldEnder- fieldEnder = void (oneOfSet (newlineOr sep)) <|> eof- in Unquoted . str <$>- many (- nonSpaceFieldChar- <|> (notFollowedBy (try terminalWhitespace)) *> oneSpace- )---- | Parse a field, be it quoted or unquoted-field :: CharParsing m => Separator -> (String -> s) -> m (Field s)-field sep str =- choice [- singleQuotedField str- , doubleQuotedField str- , unquotedField sep str- ]---- | Parse a field with its surrounding spacing-spacedField :: CharParsing m => Separator -> (String -> s) -> m (Spaced (Field s))-spacedField sep str = spaced sep (field sep str)--newlineOr :: Char -> CharSet-newlineOr c = CharSet.insert c newlines--newlines :: CharSet-newlines = CharSet.fromList "\r\n"--newline :: CharParsing m => m Newline-newline =- CRLF <$ try (string "\r\n")- <|> CR <$ char '\r'- <|> LF <$ char '\n'--space :: CharParsing m => Separator -> m HorizontalSpace-space sep =- let removeIfSep c s = if sep == c then empty else char c $> s- in removeIfSep ' ' Space <|> removeIfSep '\t' Tab--spaces :: CharParsing m => Separator -> m Spaces-spaces = fmap V.fromList . many . space---- | Combinator to parse some data surrounded by spaces-spaced :: CharParsing m => Separator -> m a -> m (Spaced a)-spaced sep p = betwixt <$> spaces sep <*> p <*> spaces sep---- | Parse an entire record, or "row"-record :: CharParsing m => ParseOptions s -> m (Record s)-record opts =- let sep = view separator opts- str = view encodeString opts- in Record . V.fromNel <$> (spacedField sep str `sepEndByNonEmpty` char sep)---- | Parse many records, or "rows"-records :: CharParsing m => ParseOptions s -> m (Records s)-records opts =- let manyV = fmap V.fromList . many- in fromMaybe EmptyRecords <$>- optional (Records <$> firstRecord opts <*> manyV (subsequentRecord opts))--firstRecord :: CharParsing m => ParseOptions s -> m (Record s)-firstRecord opts = notFollowedBy (try (ending opts)) *> record opts--subsequentRecord :: CharParsing m => ParseOptions s -> m (Newline, Record s)-subsequentRecord opts =- (,)- <$> (notFollowedBy (try (ending opts)) *> newline) -- ((if view endOnBlankLine opts then (void newline) else empty) <|> eof))- <*> record opts---- | Parse zero or many newlines-ending :: CharParsing m => ParseOptions s -> m [Newline]-ending opts =- let end = if view endOnBlankLine opts then pure [] else many newline- in [] <$ eof- <|> try (pure <$> newline <* eof)- <|> (:) <$> newline <*> ((:) <$> newline <*> end)---- | Maybe parse the header row of a CSV file, depending on the given 'Headedness'-header :: CharParsing m => ParseOptions s -> m (Maybe (Header s))-header opts = case view headedness opts of- Unheaded -> pure noHeader- Headed -> mkHeader <$> record opts <*> newline---- | Parse an 'Sv'-separatedValues :: CharParsing m => ParseOptions s -> m (Sv s)-separatedValues opts =- Sv (view separator opts) <$> header opts <*> records opts <*> ending opts
− src/Data/Sv/Parse/Options.hs
@@ -1,97 +0,0 @@-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE DefaultSignatures #-}--{-|-Module : Data.Sv.Parse.Options-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--Configuration to tell the parser what your file looks like.--}--module Data.Sv.Parse.Options (- ParseOptions (ParseOptions, _headedness, _endOnBlankLine, _parseSeparator, _encodeString)-, HasParseOptions (parseOptions, endOnBlankLine, encodeString)-, HasSeparator (..)-, HasHeadedness (..)-, defaultParseOptions-, defaultHeadedness-, defaultSeparator-) where--import Control.Lens (Lens, lens)-import Data.ByteString.UTF8 (ByteString, fromString)--import Data.Sv.Syntax.Sv (HasSeparator (separator), HasHeadedness (headedness), Headedness (Headed), Separator, comma)---- | An 'ParseOptions' informs the parser how to parse your file. The type--- parameter will be some sort of string; often 'Data.ByteString.ByteString'.------ A default is provided as 'defaultParseOptions', seen below.-data ParseOptions s =- ParseOptions {- -- | Which separator does the file use? Usually this is 'comma', but it can- -- also be 'pipe', or any other 'Char' ('Separator' = 'Char')- _parseSeparator :: Separator-- -- | Whether there is a header row with column names or not.- , _headedness :: Headedness-- -- | If a blank line is encountered, should the parse finish, or treat it as- -- an empty row and continue?- , _endOnBlankLine :: Bool-- -- | How should I turn a String into this type? This is a detail used by the parser.- , _encodeString :: String -> s- }--instance Functor ParseOptions where- fmap f (ParseOptions s h e enc) = ParseOptions s h e (f . enc)---- | Classy lenses for 'ParseOptions'-class (HasSeparator s, HasHeadedness s) => HasParseOptions s t a b | s -> a, t -> b, s b -> t, t a -> s where- parseOptions :: Lens s t (ParseOptions a) (ParseOptions b)- encodeString :: Lens s t (String -> a) (String -> b)- {-# INLINE encodeString #-}- endOnBlankLine :: s ~ t => Lens s t Bool Bool- {-# INLINE endOnBlankLine #-}- encodeString = parseOptions . encodeString- default endOnBlankLine :: (s ~ t, a ~ b) => Lens s t Bool Bool- endOnBlankLine = parseOptions . endOnBlankLine--instance HasParseOptions (ParseOptions a) (ParseOptions b) a b where- parseOptions = id- {-# INLINE parseOptions #-}- encodeString = lens _encodeString (\c s -> c { _encodeString = s })- {-# INLINE encodeString #-}- endOnBlankLine = lens _endOnBlankLine (\c b -> c { _endOnBlankLine = b })- {-# INLINE endOnBlankLine #-}--instance HasSeparator (ParseOptions s) where- separator =- lens _parseSeparator (\c s -> c { _parseSeparator = s })- {-# INLINE separator #-}--instance HasHeadedness (ParseOptions s) where- headedness =- lens _headedness (\c h -> c { _headedness = h })- {-# INLINE headedness #-}---- | 'defaultParseOptions' is used to parse a CSV file featuring a header row, using--- Trifecta as the parsing library. It uses UTF-8 'ByteString's-defaultParseOptions :: ParseOptions ByteString-defaultParseOptions = ParseOptions defaultSeparator defaultHeadedness False fromString---- | The default separator. Alias for 'comma'.-defaultSeparator :: Separator-defaultSeparator = comma---- | The default is that a header is present.-defaultHeadedness :: Headedness-defaultHeadedness = Headed
− src/Data/Sv/Print.hs
@@ -1,104 +0,0 @@-{-|-Module : Data.Sv.Print-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--Printing is the process of turning an 'Data.Sv.Syntax.Sv.Sv' into a textual-representation, such as a 'Data.ByteString.ByteString'.--If you want to turn your data type into a textual representation, you should-look instead at "Data.Sv.Encode"--}--module Data.Sv.Print (- printSv-, printSvLazy-, printSv'-, printSvLazy'-, printSvText-, printSvTextLazy-, writeSvToFile-, writeSvToHandle-, writeSvToFile'-, writeSvToHandle'-, module Data.Sv.Print.Options-) where--import Data.ByteString (ByteString)-import qualified Data.ByteString.Lazy as LBS-import Data.ByteString.Builder as Builder-import Data.Semigroup ((<>))-import Data.Text (Text)-import System.IO (BufferMode (BlockBuffering), Handle, hClose, hSetBinaryMode, hSetBuffering, openFile, IOMode (WriteMode))--import Data.Sv.Print.Options-import Data.Sv.Print.Internal-import Data.Sv.Syntax.Sv (Sv (Sv))---- | Converts an 'Sv' to a ByteString 'Builder'. Useful if you want to concatenate other--- text before or after.-svToBuilder :: PrintOptions s -> Sv s -> Builder-svToBuilder opts (Sv sep h rs e) =- foldMap (printHeader opts sep) h <> printRecords opts sep rs <> foldMap printNewline e---- | Writes an sv to a file handle. This goes directly from a 'Builder', so it is--- more efficient than calling 'printSv' or 'printSvLazy' and writing the--- result to the handle.-writeSvToHandle :: Handle -> Sv ByteString -> IO ()-writeSvToHandle = writeSvToHandle' defaultPrintOptions---- | Writes an sv to a file. This goes directly from a 'Builder', so it is--- more efficient than calling 'printSv' or 'printSvLazy' and writing the--- result to a file.-writeSvToFile :: FilePath -> Sv ByteString -> IO ()-writeSvToFile = writeSvToFile' defaultPrintOptions---- | Writes an sv to a file handle. This goes directly from a 'Builder', so it is--- more efficient than calling 'printSv' or 'printSvLazy' and writing the--- result to the handle.------ This version is polymorphic, but as a penalty you have to tell me how to--- get a Bytestring 'Builder'.-writeSvToHandle' :: PrintOptions s -> Handle -> Sv s -> IO ()-writeSvToHandle' opts h sv = hPutBuilder h (svToBuilder opts sv)---- | Writes an sv to a file. This goes directly from a 'Builder', so it is--- more efficient than calling 'printSv' or 'printSvLazy' and writing the--- result to a file.------ This version is polymorphic, but as a penalty you have to tell me how to--- get a Bytestring 'Builder'.-writeSvToFile' :: PrintOptions s -> FilePath -> Sv s -> IO ()-writeSvToFile' opts fp sv = do- h <- openFile fp WriteMode- hSetBuffering h (BlockBuffering Nothing)- hSetBinaryMode h True- writeSvToHandle' opts h sv- hClose h---- | Print an Sv to a 'Data.ByteString.ByteString' value.-printSv :: Sv ByteString -> ByteString-printSv = printSv' defaultPrintOptions---- | Print an Sv to a lazy 'Data.ByteString.Lazy.ByteString' value.-printSvLazy :: Sv ByteString -> LBS.ByteString-printSvLazy = printSvLazy' defaultPrintOptions---- | Converts the given 'Sv' into a strict 'Data.ByteString.ByteString'-printSv' :: PrintOptions s -> Sv s -> ByteString-printSv' opts = LBS.toStrict . printSvLazy' opts---- | Converts the given 'Sv' into a lazy 'Data.ByteString.Lazy.ByteString'-printSvLazy' :: PrintOptions s -> Sv s -> LBS.ByteString-printSvLazy' opts = toLazyByteString . svToBuilder opts---- | Print an Sv containing 'Text' to a 'Data.ByteString.ByteString'-printSvText :: Sv Text -> ByteString-printSvText = printSv' textPrintOptions---- | Print an Sv containing 'Text' to a 'Data.ByteString.Lazy.ByteString'-printSvTextLazy :: Sv Text -> LBS.ByteString-printSvTextLazy = printSvLazy' textPrintOptions
− src/Data/Sv/Print/Internal.hs
@@ -1,73 +0,0 @@-{-|-Module : Data.Sv.Print.Internal-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--This module is considered an implementation detail.-As the "Internal" module name suggests, this module is exempt from-the PVP, so depend on it at your own risk!-These functions exist to be called by "Data.Sv.Print".--}--module Data.Sv.Print.Internal (- printNewline- , printField- , printSpaced- , printRecord- , printRecords- , printHeader-) where--import Control.Lens (review, view)-import Data.Bifoldable (bifoldMap)-import Data.ByteString.Builder as Builder-import Data.Semigroup ((<>))-import Data.Semigroup.Foldable (intercalate1)--import Data.Sv.Print.Options-import Data.Sv.Syntax.Field (Field (Quoted, Unquoted), SpacedField)-import Data.Sv.Syntax.Record (Record (Record), Records (Records, EmptyRecords))-import Data.Sv.Syntax.Sv (Header (Header), Separator)-import Text.Newline-import Text.Space (spaceToChar, Spaced (Spaced))-import Text.Quote---- | Convert a 'Newline' to a ByteString 'Builder'-printNewline :: Newline -> Builder-printNewline = Builder.lazyByteString . newlineToString---- | Convert a 'Field' to a ByteString 'Builder'-printField :: PrintOptions s -> Field s -> Builder-printField opts f =- case f of- Unquoted s ->- view build opts s- Quoted q s ->- let qc = quoteToString q- contents = view build opts $ view escape opts (review quoteChar q) s- in qc <> contents <> qc---- | Convert a 'SpacedField' to a ByteString 'Builder'-printSpaced :: PrintOptions s -> SpacedField s -> Builder-printSpaced opts (Spaced b t a) =- let spc = foldMap (Builder.charUtf8 . spaceToChar)- in spc b <> printField opts a <> spc t---- | Convert a 'Record' to a ByteString 'Builder'-printRecord :: PrintOptions s -> Separator -> Record s -> Builder-printRecord opts sep (Record fs) =- intercalate1 (Builder.charUtf8 sep) (fmap (printSpaced opts) fs)---- | Convert 'Records' to a ByteString 'Builder'.-printRecords :: PrintOptions s -> Separator -> Records s -> Builder-printRecords opts sep rs = case rs of- EmptyRecords -> mempty- Records a as ->- printRecord opts sep a <> foldMap (bifoldMap printNewline (printRecord opts sep)) as---- | Convert 'Header' to a ByteString 'Builder'.-printHeader :: PrintOptions s -> Separator -> Header s -> Builder-printHeader opts sep (Header r n) = printRecord opts sep r <> printNewline n
− src/Data/Sv/Print/Options.hs
@@ -1,80 +0,0 @@-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE FlexibleInstances #-}--{-|-Module : Data.Sv.Print.Options-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--}--module Data.Sv.Print.Options (- PrintOptions (..)- , HasPrintOptions (..)- , defaultPrintOptions- , utf8PrintOptions- , utf8LazyPrintOptions- , textPrintOptions- , stringPrintOptions-) where--import Control.Lens-import qualified Data.ByteString as BS-import qualified Data.ByteString.Lazy as BL-import Data.ByteString.Builder (Builder)-import qualified Data.ByteString.Builder as Builder-import Data.Text (Text)-import qualified Data.Text.Encoding as Text--import Text.Escape (Escaper', escapeUtf8, escapeUtf8Lazy, escapeText, escapeString)---- | Options to configure the printing process-data PrintOptions a =- PrintOptions {- -- | How do I convert these values into ByteString 'Builder's? This depends- -- not only on type, but on character encoding. Default: 'utf8PrintOptions'- _build :: a -> Builder- -- | How do I escape quotes which appear in this value? Default: 'escapeUtf8'- , _escape :: Escaper' a- }---- | Classy optics for 'PrintOptions'-class HasPrintOptions c a | c -> a where- printOptions :: Lens' c (PrintOptions a)- build :: Lens' c (a -> Builder)- {-# INLINE build #-}- escape :: Lens' c (Escaper' a)- {-# INLINE escape #-}- build = printOptions . build- escape = printOptions . escape--instance HasPrintOptions (PrintOptions a) a where- printOptions = id- {-# INLINE printOptions #-}- build f (PrintOptions x1 x2) = fmap (\ y -> PrintOptions y x2) (f x1)- {-# INLINE build #-}- escape f (PrintOptions x1 x2) = fmap (PrintOptions x1) (f x2)- {-# INLINE escape #-}---- | Print options for 'Sv's containing UTF-8 bytestrings-defaultPrintOptions :: PrintOptions BS.ByteString-defaultPrintOptions = utf8PrintOptions---- | Print options for 'Sv's containing UTF-8 bytestrings-utf8PrintOptions :: PrintOptions BS.ByteString-utf8PrintOptions = PrintOptions Builder.byteString escapeUtf8---- | Print options for 'Sv's containing UTF-8 lazy bytestrings-utf8LazyPrintOptions :: PrintOptions BL.ByteString-utf8LazyPrintOptions = PrintOptions Builder.lazyByteString escapeUtf8Lazy---- | Print options for 'Sv's containing 'Text'-textPrintOptions :: PrintOptions Text-textPrintOptions = PrintOptions (Builder.byteString . Text.encodeUtf8) escapeText---- | Print options for 'Sv's containing 'String's-stringPrintOptions :: PrintOptions String-stringPrintOptions = PrintOptions Builder.stringUtf8 escapeString
+ src/Data/Sv/Structure.hs view
@@ -0,0 +1,14 @@+{-|+Module : Data.Sv.Structure+Copyright : (C) CSIRO 2017-2018+License : BSD3+Maintainer : George Wilson <george.wilson@data61.csiro.au>+Stability : experimental+Portability : non-portable+-}++module Data.Sv.Structure (+ module Data.Sv.Structure.Core+) where++import Data.Sv.Structure.Core
− src/Data/Sv/Syntax.hs
@@ -1,18 +0,0 @@-{-|-Module : Data.Sv.Syntax-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--}--module Data.Sv.Syntax (- module Data.Sv.Syntax.Field-, module Data.Sv.Syntax.Record-, module Data.Sv.Syntax.Sv-) where--import Data.Sv.Syntax.Field-import Data.Sv.Syntax.Record-import Data.Sv.Syntax.Sv
− src/Data/Sv/Syntax/Field.hs
@@ -1,116 +0,0 @@-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE DeriveGeneric #-}--{-|-Module : Data.Sv.Syntax.Field-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--}--module Data.Sv.Syntax.Field (- Field (Unquoted, Quoted)- , SpacedField- , Spaced (Spaced)- , HasFields (fields)- , AsField (_Field, _Unquoted, _Quoted)- , unescapedField- , foldField- , fieldContents-) where--import Control.DeepSeq (NFData)-import Control.Lens (Lens, Prism', Traversal, lens, prism)-import Data.Foldable (Foldable (foldMap))-import Data.Functor (Functor (fmap))-import Data.Traversable (Traversable (traverse))-import GHC.Generics (Generic)--import Text.Escape (Unescaped (Unescaped, getRawUnescaped))-import Text.Quote (Quote)-import Text.Space (Spaced (Spaced))---- | A 'Field' is a single cell from a CSV document.------ Its value is either 'Quoted', which indicates the type of quote--- surrounding the value, or it is 'Unquoted', containing only the value.-data Field s =- Unquoted s- | Quoted Quote (Unescaped s)- deriving (Eq, Ord, Show, Generic)--instance NFData s => NFData (Field s)--instance Functor Field where- fmap f fi = case fi of- Unquoted s -> Unquoted (f s)- Quoted q v -> Quoted q (fmap f v)--instance Foldable Field where- foldMap f fi = case fi of- Unquoted s -> f s- Quoted _ v -> foldMap f v--instance Traversable Field where- traverse f fi = case fi of- Unquoted s -> Unquoted <$> f s- Quoted q v -> Quoted q <$> traverse f v---- | 'Field's are often surrounded by spaces-type SpacedField a = Spaced (Field a)---- | Classy prisms for 'Field'-class (HasFields s s a a) => AsField s a | s -> a where- _Field :: Prism' s (Field a)- _Unquoted :: Prism' s a- _Quoted :: Prism' s (Quote, Unescaped a)- _Unquoted = _Field . _Unquoted- {-# INLINE _Unquoted #-}- _Quoted = _Field . _Quoted- {-# INLINE _Quoted #-}--instance AsField (Field a) a where- _Field = id- {-# INLINE _Field #-}- _Unquoted = prism Unquoted- (\x -> case x of- Unquoted y -> Right y- _ -> Left x- )- {-# INLINE _Unquoted #-}- _Quoted = prism (uncurry Quoted)- (\x -> case x of- Quoted y z -> Right (y,z)- _ -> Left x- )- {-# INLINE _Quoted #-}---- | Classy 'Traversal'' for things containing 'Field's-class HasFields c d s t | c -> s, d -> t, c t -> d, d s -> c where- fields :: Traversal c d (Field s) (Field t)--instance HasFields (Field s) (Field t) s t where- fields = id- {-# INLINE fields #-}---- | Build a quoted field with a normal string-unescapedField :: Quote -> s -> Field s-unescapedField q s = Quoted q (Unescaped s)---- | The catamorphism for @Field'@-foldField :: (s -> b) -> (Quote -> Unescaped s -> b) -> Field s -> b-foldField u q fi = case fi of- Unquoted s -> u s- Quoted a b -> q a b---- | Lens into the contents of a Field, regardless of whether it's quoted or unquoted-fieldContents :: Lens (Field s) (Field t) s t-fieldContents =- lens (foldField id (const getRawUnescaped)) $ \f b -> case f of- Unquoted _ -> Unquoted b- Quoted q _ -> Quoted q (Unescaped b)
− src/Data/Sv/Syntax/Record.hs
@@ -1,176 +0,0 @@-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE DeriveGeneric #-}--{-|-Module : Data.Sv.Syntax.Record-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--This module contains datatypes for Records. A record is a "line" or "row"-of a CSV document--}-module Data.Sv.Syntax.Record (- Record (Record, _fields)- -- Optics- , HasRecord (record, spacedFields)- , recordSpacedFieldsIso- , emptyRecord- , singleField- , recordNel- , Records (EmptyRecords, Records)- , HasRecords (records, traverseRecords, traverseNewlines)- , _EmptyRecords- , _NonEmptyRecords- , mkRecords- , singleRecord- , recordList-) where--import Control.DeepSeq (NFData)-import Control.Lens (Lens, Lens', Iso, Prism, Prism', Traversal', _1, _2, beside, iso, prism, prism', toListOf)-import Data.Foldable (Foldable (foldMap))-import Data.Functor (Functor (fmap))-import Data.List.NonEmpty (NonEmpty ((:|)))-import Data.Semigroup (Semigroup)-import Data.Traversable (Traversable (traverse))-import GHC.Generics (Generic)--import Data.Sv.Syntax.Field (SpacedField, Field (Unquoted), HasFields (fields))-import Data.Vector.NonEmpty (NonEmptyVector)-import Data.Vector (Vector)-import qualified Data.Vector as V-import qualified Data.Vector.NonEmpty as V-import Text.Newline (Newline)-import Text.Space (Spaced, spacedValue)---- | A @Record@ is a non-empty collection of Fields, implicitly separated--- by a separator (often a comma).-newtype Record s =- Record {- _fields :: NonEmptyVector (Spaced (Field s))- }- deriving (Eq, Ord, Show, Semigroup, Generic)--instance NFData s => NFData (Record s)---- | A 'Record' is isomorphic to a 'NonEmpty' list of 'SpacedField's-recordSpacedFieldsIso :: Iso (Record s) (Record a) (NonEmptyVector (Spaced (Field s))) (NonEmptyVector (Spaced (Field a)))-recordSpacedFieldsIso = iso _fields Record-{-# INLINE recordSpacedFieldsIso #-}---- | Classy lenses for 'Record'-class HasRecord s t a b | s -> a, t -> b where- record :: Lens s t (Record a) (Record b)- spacedFields :: Lens s t (NonEmptyVector (Spaced (Field a))) (NonEmptyVector (Spaced (Field b)))- {-# INLINE spacedFields #-}- spacedFields = record . spacedFields--instance HasRecord (Record a) (Record b) a b where- record = id- {-# INLINE record #-}- spacedFields = recordSpacedFieldsIso- {-# INLINE spacedFields #-}--instance HasFields (Record a) (Record b) a b where- fields = spacedFields . traverse . spacedValue--instance Functor Record where- fmap f = Record . fmap (fmap (fmap f)) . _fields--instance Foldable Record where- foldMap f = foldMap (foldMap (foldMap f)) . _fields--instance Traversable Record where- traverse f = fmap Record . traverse (traverse (traverse f)) . _fields---- | Build an empty record.------ According to RFC 4180, a record must have at least one field.--- But a field can be the empty string. So this is the closest we can get to--- an empty record.------ Note that this does not make 'Record' a 'Monoid'. It is not a lawful unit--- for the 'Semigroup' operation.-emptyRecord :: Monoid s => Record s-emptyRecord = singleField (Unquoted mempty)---- | Build a 'Record' with just one 'Field'-singleField :: Field s -> Record s-singleField = Record . pure . pure---- | Build a 'Record' given a 'NonEmpty' list of its fields-recordNel :: NonEmpty (SpacedField s) -> Record s-recordNel = Record . V.fromNel---- | A collection of records, separated by newlines.-data Records s =- EmptyRecords- | Records (Record s) (Vector (Newline, Record s))- deriving (Eq, Ord, Show, Generic)--instance NFData s => NFData (Records s)---- | Prism for an empty 'Records'-_EmptyRecords :: Prism' (Records s) ()-_EmptyRecords =- prism' (const EmptyRecords) $ \r ->- case r of- EmptyRecords -> Just ()- Records _ _ -> Nothing---- | Prism for a non-empty 'Records'-_NonEmptyRecords :: Prism (Records s) (Records t) (Record s, Vector (Newline, Record s)) (Record t, Vector (Newline, Record t))-_NonEmptyRecords =- prism (uncurry Records) $ \r ->- case r of- EmptyRecords -> Left EmptyRecords- Records a as -> Right (a,as)---- | Classy lenses for 'Records'-class HasRecords c s | c -> s where- records :: Lens' c (Records s)- traverseRecords :: Traversal' c (Record s)- traverseRecords = records . _NonEmptyRecords . beside id (traverse . _2)- {-# INLINE traverseRecords #-}- traverseNewlines :: Traversal' c Newline- traverseNewlines = records . _NonEmptyRecords . _2 . traverse . _1--instance HasRecords (Records s) s where- records = id- {-# INLINE records #-}--instance Functor Records where- fmap f rs = case rs of- EmptyRecords -> EmptyRecords- Records a as -> Records (fmap f a) (fmap (fmap (fmap f)) as)--instance Foldable Records where- foldMap f rs = case rs of- EmptyRecords -> mempty- Records a as -> foldMap f a `mappend` foldMap (foldMap (foldMap f)) as--instance Traversable Records where- traverse f rs = case rs of- EmptyRecords -> pure EmptyRecords- Records a as -> Records <$> traverse f a <*> traverse (traverse (traverse f)) as---- | Convenience constructor for 'Records'.------ This puts the same newline between all the records.-mkRecords :: Newline -> NonEmpty (Record s) -> Records s-mkRecords n (r:|rs) = Records r (V.fromList (fmap (n,) rs))---- | A record collection conaining one record-singleRecord :: Record s -> Records s-singleRecord s = Records s V.empty---- | Collect the list of 'Record's from anything that 'HasRecords'-recordList :: HasRecords c s => c -> [Record s]-recordList = toListOf traverseRecords
− src/Data/Sv/Syntax/Sv.hs
@@ -1,219 +0,0 @@-{-# LANGUAGE DefaultSignatures #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE FlexibleInstances#-}-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE DeriveFoldable #-}-{-# LANGUAGE DeriveTraversable #-}-{-# LANGUAGE DeriveGeneric #-}--{-|-Module : Data.Sv.Syntax.Sv-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--This file defines a datatype for a complete Sv document.-The datatype preserves information such as whitespace so that the original-text can be recovered.--In the usual workflow, this type is only an intermediate stage between-parsing and decoding. You can program against it directly using the provided-functions and optics if you'd like. For an example of this see-<https://github.com/qfpl/sv/blob/master/examples/src/Data/Sv/Example/Requote.hs Requote.hs>--}--module Data.Sv.Syntax.Sv (- Sv (Sv, _separatorSv, _maybeHeader, _records, _finalNewlines)- , HasSv (sv, maybeHeader, traverseHeader, finalNewlines)- , HasRecords (records, traverseNewlines, traverseRecords)- , mkSv- , emptySv- , recordList- , Header (Header, _headerRecord)- , HasHeader (header, headerRecord, headerNewline)- , noHeader- , mkHeader- , Headedness (Unheaded, Headed)- , HasHeadedness (headedness)- , getHeadedness- , Separator- , HasSeparator (separator)- , comma- , pipe- , tab-) where--import Control.DeepSeq (NFData)-import Control.Lens (Lens, Lens', Traversal')-import Data.Foldable (Foldable (foldMap))-import Data.Functor (Functor (fmap), (<$>))-import Data.Monoid ((<>))-import Data.Traversable (Traversable (traverse))-import GHC.Generics (Generic)--import Data.Sv.Syntax.Field (HasFields (fields))-import Data.Sv.Syntax.Record (Record, Records (EmptyRecords), HasRecord (record), HasRecords (records, traverseNewlines, traverseRecords), recordList)-import Text.Newline (Newline)---- | 'Sv' is a whitespace-preserving data type for separated values.--- Often the separator is a comma, but this type does not make that--- assumption so that it can be used for pipe- or tab-separated values--- as well.-data Sv s =- Sv {- _separatorSv :: Separator- , _maybeHeader :: Maybe (Header s)- , _records :: Records s- , _finalNewlines :: [Newline]- }- deriving (Eq, Ord, Show, Generic)--instance NFData s => NFData (Sv s)---- | Classy lenses for 'Sv'-class (HasRecords c s, HasSeparator c) => HasSv c s | c -> s where- sv :: Lens' c (Sv s)- maybeHeader :: Lens' c (Maybe (Header s))- {-# INLINE maybeHeader #-}- traverseHeader :: Traversal' c (Header s)- {-# INLINE traverseHeader #-}- finalNewlines :: Lens' c [Newline]- {-# INLINE finalNewlines #-}- maybeHeader = sv . maybeHeader- traverseHeader = maybeHeader . traverse- finalNewlines = sv . finalNewlines--instance HasRecords (Sv s) s where- records f (Sv x1 x2 x3 x4) =- fmap (\y -> Sv x1 x2 y x4) (f x3)- {-# INLINE records #-}--instance HasSv (Sv s) s where- sv = id- {-# INLINE sv #-}- maybeHeader f (Sv x1 x2 x3 x4) =- fmap (\y -> Sv x1 y x3 x4) (f x2)- {-# INLINE maybeHeader #-}- finalNewlines f (Sv x1 x2 x3 x4) =- fmap (Sv x1 x2 x3) (f x4)- {-# INLINE finalNewlines #-}---- | Convenience constructor for Sv-mkSv :: Separator -> Maybe (Header s) -> [Newline] -> Records s -> Sv s-mkSv c h ns rs = Sv c h rs ns---- | An empty Sv-emptySv :: Separator -> Sv s-emptySv c = Sv c Nothing EmptyRecords []--instance Functor Sv where- fmap f (Sv s h rs e) = Sv s (fmap (fmap f) h) (fmap f rs) e--instance Foldable Sv where- foldMap f (Sv _ h rs _) = foldMap (foldMap f) h <> foldMap f rs--instance Traversable Sv where- traverse f (Sv s h rs e) = Sv s <$> traverse (traverse f) h <*> traverse f rs <*> pure e---- | Determine the 'Headedness' of an 'Sv'-getHeadedness :: Sv s -> Headedness-getHeadedness = maybe Unheaded (const Headed) . _maybeHeader---- | A 'Header' is present in many CSV documents, usually listing the names--- of the columns. We keep this separate from the regular records.-data Header s =- Header {- _headerRecord :: Record s- , _headerNewline :: Newline- }- deriving (Eq, Ord, Show, Functor, Foldable, Traversable, Generic)--instance NFData s => NFData (Header s)---- | Classy lenses for 'Header'-class HasHeader s t a b | s -> a, t -> b, s b -> t, t a -> s where- header :: Lens s t (Header a) (Header b)- headerNewline :: (s ~ t) => Lens s t Newline Newline- {-# INLINE headerNewline #-}- headerRecord :: Lens s t (Record a) (Record b)- {-# INLINE headerRecord #-}- default headerNewline :: (a ~ b) => Lens s t Newline Newline- headerNewline = header . headerNewline- headerRecord = header . headerRecord--instance HasHeader (Header a) (Header b) a b where- header = id- {-# INLINE header #-}- headerNewline f (Header x1 x2)- = fmap (Header x1) (f x2)- {-# INLINE headerNewline #-}- headerRecord f (Header x1 x2)- = fmap (\y -> Header y x2) (f x1)- {-# INLINE headerRecord #-}--instance HasRecord (Header a) (Header b) a b where- record = headerRecord- {-# INLINE record #-}--instance HasFields (Header a) (Header b) a b where- fields = headerRecord . fields---- | Used to build 'Sv's that don't have a header-noHeader :: Maybe (Header s)-noHeader = Nothing---- | Convenience constructor for 'Header', usually when you're building 'Sv's-mkHeader :: Record s -> Newline -> Maybe (Header s)-mkHeader r n = Just (Header r n)---- | Does the 'Sv' have a 'Header' or not? A header is a row at the beginning--- of a file which contains the string names of each of the columns.------ If a header is present, it must not be decoded with the rest of the data.-data Headedness =- Unheaded | Headed- deriving (Eq, Ord, Show)---- | Classy lens for 'Headedness'-class HasHeadedness c where- headedness :: Lens' c Headedness--instance HasHeadedness Headedness where- headedness = id---- | By what are your values separated? The answer is often 'comma', but not always.------ A 'Separator' is just a 'Char'. It could be a sum type instead, since it--- will usually be comma or pipe, but our preference has been to be open here--- so that you can use whatever you'd like. There are test cases, for example,--- ensuring that you're free to use null-byte separated values if you so desire.-type Separator = Char---- | Classy lens for 'Separator'-class HasSeparator c where- separator :: Lens' c Separator--instance HasSeparator Char where- separator = id- {-# INLINE separator #-}--instance HasSeparator (Sv s) where- separator f (Sv x1 x2 x3 x4) =- fmap (\y -> Sv y x2 x3 x4) (f x1)- {-# INLINE separator #-}---- | The venerable comma separator. Used for CSV documents.-comma :: Separator-comma = ','---- | The pipe separator. Used for PSV documents.-pipe :: Separator-pipe = '|'---- | Tab is a separator too - why not?-tab :: Separator-tab = '\t'
− src/Data/Vector/NonEmpty.hs
@@ -1,79 +0,0 @@-{-# LANGUAGE DeriveGeneric #-}--{-|-Module : Data.Vector.NonEmpty-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--}--module Data.Vector.NonEmpty (- NonEmptyVector (NonEmptyVector)-, fromNel-, toNel-, headNev-, tailNev-) where--import Control.DeepSeq (NFData)-import Control.Lens (Lens', lens)-import Data.Functor.Apply (Apply((<.>)))-import Data.Foldable (toList)-import Data.List.NonEmpty (NonEmpty ((:|)))-import Data.Semigroup (Semigroup ((<>)))-import Data.Semigroup.Foldable (Foldable1 (foldMap1))-import Data.Semigroup.Traversable (Traversable1 (traverse1))-import Data.Vector (Vector)-import qualified Data.Vector as V-import GHC.Generics (Generic)---- | A non-empty value of 'Vector'-data NonEmptyVector a =- NonEmptyVector a (Vector a)- deriving (Eq, Ord, Show, Generic)--instance NFData a => NFData (NonEmptyVector a) where---- | Convert a 'NonEmpty' list to a 'NonEmptyVector'-fromNel :: NonEmpty a -> NonEmptyVector a-fromNel (a :| as) = NonEmptyVector a (V.fromList as)---- | Convert a 'NonEmptyVector' to a 'NonEmpty' list-toNel :: NonEmptyVector a -> NonEmpty a-toNel (NonEmptyVector a as) = a :| V.toList as--instance Functor NonEmptyVector where- fmap f (NonEmptyVector a as) = NonEmptyVector (f a) (fmap f as)--instance Apply NonEmptyVector where- (<.>) = (<*>)--instance Applicative NonEmptyVector where- pure a = NonEmptyVector a V.empty- ff <*> fa = fromNel (toNel ff <*> toNel fa)--instance Foldable NonEmptyVector where- foldMap f (NonEmptyVector a as) = f a `mappend` foldMap f as--instance Foldable1 NonEmptyVector where- foldMap1 f (NonEmptyVector a as) = foldMap1 f (a :| toList as)--instance Traversable NonEmptyVector where- traverse f (NonEmptyVector a as) = NonEmptyVector <$> f a <*> traverse f as--instance Traversable1 NonEmptyVector where- traverse1 f = fmap fromNel . traverse1 f . toNel--instance Semigroup (NonEmptyVector a) where- NonEmptyVector a as <> NonEmptyVector b bs =- NonEmptyVector a (V.concat [as, V.singleton b, bs])---- | Get or set the head of a 'NonEmptyVector'-headNev :: Lens' (NonEmptyVector a) a-headNev = lens (\(NonEmptyVector h _) -> h) (\(NonEmptyVector _ t) h -> NonEmptyVector h t)---- | Get or set the head of a 'NonEmptyVector'-tailNev :: Lens' (NonEmptyVector a) (Vector a)-tailNev = lens (\(NonEmptyVector _ t) -> t) (\(NonEmptyVector h _) t -> NonEmptyVector h t)
− src/Text/Escape.hs
@@ -1,124 +0,0 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE DeriveFoldable #-}-{-# LANGUAGE DeriveTraversable #-}-{-# LANGUAGE DeriveGeneric #-}--{-|-Module : Text.Escape-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--Quote characters can be escaped in CSV documents by using two quote characters-instead of one. sv's parser will unescape these sequences as it parses them, so-it wraps them in the newtype 'Unescaped'--Encoding requires you to provide an 'Escaper', which is a function to escape-strings on the way out.--}--module Text.Escape (- Unescaped (Unescaped, getRawUnescaped)- , Escaper- , Escaper'- , escapeString- , escapeText- , escapeUtf8- , escapeUtf8Lazy- , escapeChar-) where--import Control.DeepSeq (NFData)-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as L-import qualified Data.ByteString.UTF8 as UTF8-import qualified Data.ByteString.Lazy.UTF8 as UTF8L-import Data.Foldable (Foldable)-import Data.Functor (Functor)-import Data.Monoid (Monoid)-import Data.Semigroup (Semigroup ((<>)))-import Data.Text (Text)-import qualified Data.Text as Text-import Data.Traversable (Traversable)-import GHC.Generics (Generic)---- | Wrapper for text that is known to be in an unescaped form-newtype Unescaped a =- Unescaped { getRawUnescaped :: a }- deriving (Eq, Ord, Show, Semigroup, Monoid, Functor, Foldable, Traversable, Generic)--instance NFData a => NFData (Unescaped a)---- | A function that, given a char, escapes all occurrences of that char.------ This version allows the escaping to be type-changing. For example, escaping--- a single char can result in a string with two characters.-type Escaper s t = Char -> Unescaped s -> t---- | A function that, given a char, escapes all occurrences of that char.-type Escaper' a = Char -> Unescaped a -> a---- | Replaces all occurrences of the given character with two occurrences of that--- character, non-recursively, in the given 'String'.------ >>> escapeString ''' "hello 'string'"--- "hello ''string''"----escapeString :: Escaper' String-escapeString c = concatMap (doubleChar c) . getRawUnescaped---- | Replaces all occurrences of the given character with two occurrences of that--- character in the given 'Text'------ @--- {- LANGUAGE OverloadedStrings -}------ >>> escapeText ''' "hello 'text'"--- "hello ''text''"--- @-escapeText :: Escaper' Text-escapeText c =- let ct = Text.singleton c- in Text.replace ct (ct <> ct) . getRawUnescaped---- | Replaces all occurrences of the given character with two occurrences of that--- character in the given ByteString, which is assumed to be UTF-8 compatible.------ @--- {- LANGUAGE OverloadedStrings -}--- >>> escapeUtf8 ''' "hello 'bytestring'"--- "hello ''bytestring''"--- @-escapeUtf8 :: Escaper' B.ByteString-escapeUtf8 c =- UTF8.fromString . concatMap (doubleChar c) . UTF8.toString . getRawUnescaped---- | Replaces all occurrences of the given character with two occurrences of that--- character in the given lazy ByteString, which is assumed to be UTF-8 compatible.------ @--- {- LANGUAGE OverloadedStrings -}------ >>> escapeUtf8Lazy ''' "hello 'lazy bytestring'"--- "hello ''lazy bytestring''"--- @-escapeUtf8Lazy :: Escaper' L.ByteString-escapeUtf8Lazy c =- UTF8L.fromString . concatMap (doubleChar c) . UTF8L.toString . getRawUnescaped---- | Escape a character, which must return a string.------ >>> escapeChar ''' '''--- "''"------ >>> escapeChar ''' 'z'--- "z"----escapeChar :: Escaper Char String-escapeChar c = doubleChar c . getRawUnescaped--doubleChar :: Char -> Char -> String-doubleChar q z = if z == q then [q,q] else [z]
− src/Text/Newline.hs
@@ -1,84 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--{-|-Module : Text.Newline-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--A sum type for line endings--}--module Text.Newline (- Newline (CR, LF, CRLF)- , AsNewline(_Newline, _CR, _LF, _CRLF)- , newlineToString- , parseNewline-) where--import Control.DeepSeq (NFData (rnf))-import Control.Lens (Prism', prism, prism')-import Data.String (IsString (fromString))-import Data.Text (Text)---- | 'Newline' is a sum type for line endings-data Newline =- -- | > "\r"- CR- -- | > "\n"- | LF- -- | > "\rn"- | CRLF- deriving (Eq, Ord, Show)--instance NFData Newline where- rnf x = seq x ()---- | 'AsNewline' is a classy prism for 'Newline'-class AsNewline r where- _Newline :: Prism' r Newline- _CR :: Prism' r ()- _LF :: Prism' r ()- _CRLF :: Prism' r ()- _CR = _Newline . _CR- _LF = _Newline . _LF- _CRLF = _Newline . _CRLF--instance AsNewline Newline where- _Newline = id- _CR =- prism (const CR) $ \x -> case x of- CR -> Right ()- _ -> Left x- _LF =- prism (const LF) $ \x -> case x of- LF -> Right ()- _ -> Left x- _CRLF =- prism (const CRLF) $ \x -> case x of- CRLF -> Right ()- _ -> Left x--instance AsNewline Text where- _Newline = prism' newlineToString parseNewline---- | Convert a 'Newline' to a 'String'. Since this uses 'Data.String.IsString',--- it works for other data types, like 'Data.Text.Text' or--- 'Data.ByteString.ByteString'.-newlineToString :: IsString s => Newline -> s-newlineToString n = fromString $- case n of- CR -> "\r"- LF -> "\n"- CRLF -> "\r\n"---- | Try to parse text into a 'Newline'-parseNewline :: Text -> Maybe Newline-parseNewline "" = Nothing-parseNewline "\r" = Just CR-parseNewline "\n" = Just LF-parseNewline "\r\n" = Just CRLF-parseNewline _ = Nothing-
− src/Text/Quote.hs
@@ -1,72 +0,0 @@-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE TypeFamilies #-}--{-|-Module : Text.Quote-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--A sum type for quote characters--}--module Text.Quote (- Quote (SingleQuote, DoubleQuote)- , AsQuote (_Quote, _SingleQuote, _DoubleQuote)- , quoteChar- , quoteToString-) where--import Control.DeepSeq (NFData (rnf))-import Control.Lens (Prism', prism, prism', review)-import Data.String (IsString (fromString))---- | A sum type for quote characters. Either single or double quotes.-data Quote =- SingleQuote- | DoubleQuote- deriving (Eq, Ord, Show)--instance NFData Quote where- rnf x = seq x ()---- | Classy prisms for 'Quote'-class AsQuote r where- _Quote :: Prism' r Quote- _SingleQuote :: Prism' r ()- _DoubleQuote :: Prism' r ()- _SingleQuote = _Quote . _SingleQuote- _DoubleQuote = _Quote . _DoubleQuote--instance AsQuote Quote where- _Quote = id- _SingleQuote = prism (const SingleQuote) $ \x -> case x of- SingleQuote -> Right ()- DoubleQuote -> Left x- _DoubleQuote = prism (const DoubleQuote) $ \x -> case x of- SingleQuote -> Left x- DoubleQuote -> Right ()--instance AsQuote Char where- _Quote = quoteChar---- | Convert a Quote to the Char it represents.-quoteChar :: Prism' Char Quote-quoteChar =- prism'- (\q -> case q of- SingleQuote -> '\''- DoubleQuote -> '"')- (\c -> case c of- '\'' -> Just SingleQuote- '"' -> Just DoubleQuote- _ -> Nothing)---- | Convert a 'Quote' to a 'String'. Since this uses 'Data.String.IsString',--- it works for other data types, like 'Data.Text.Text' or--- 'Data.ByteString.ByteString'.-quoteToString :: IsString a => Quote -> a-quoteToString = fromString . pure . review quoteChar
− src/Text/Space.hs
@@ -1,190 +0,0 @@-{-# LANGUAGE DefaultSignatures #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE DeriveGeneric #-}--{-|-Module : Text.Space-Copyright : (C) CSIRO 2017-2018-License : BSD3-Maintainer : George Wilson <george.wilson@data61.csiro.au>-Stability : experimental-Portability : non-portable--A sum type for space characters--}--module Text.Space- ( HorizontalSpace (Space, Tab)- , AsHorizontalSpace (_HorizontalSpace, _Space, _Tab)- , Spaces- , single- , manySpaces- , tab- , spaceToChar- , charToSpace- , spacesText- , spacesString- , Spaced (Spaced, _before, _after, _value)- , HasSpaced (spaced, spacedValue, before, after)- , betwixt- , uniform- , unspaced- , removeSpaces- )-where--import Control.DeepSeq (NFData (rnf))-import Control.Lens (Lens, Prism', prism, prism')-import Data.Semigroup (Semigroup ((<>)))-import Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Vector as V-import GHC.Generics (Generic)---- | 'HorizontalSpace' is a subset of 'Char'. To move back and forth betwen--- it and 'Char', 'String', or 'Data.Text.Text', use '_HorizontalSpace'-data HorizontalSpace =- Space- | Tab- deriving (Eq, Ord, Show)--instance NFData HorizontalSpace where- rnf x = seq x ()---- | Classy prisms for 'HorizontalSpace's-class AsHorizontalSpace r where- _HorizontalSpace :: Prism' r HorizontalSpace- _Space :: Prism' r ()- _Tab :: Prism' r ()- _Space = _HorizontalSpace . _Space- _Tab = _HorizontalSpace . _Tab--instance AsHorizontalSpace HorizontalSpace where- _HorizontalSpace = id- _Space =- prism (const Space) $ \x ->- case x of- Space -> Right ()- _ -> Left x- _Tab =- prism (const Tab) $ \x ->- case x of- Tab -> Right ()- _ -> Left x--instance AsHorizontalSpace Char where- _HorizontalSpace = prism' spaceToChar charToSpace---- | Helpful alias for lists of 'Space's-type Spaces = V.Vector HorizontalSpace---- | One space-single :: Spaces-single = V.singleton Space---- | As many spaces as you'd like-manySpaces :: Int -> Spaces-manySpaces = flip V.replicate Space---- | One tab-tab :: Spaces-tab = V.singleton Tab---- | Turn a 'Space' into a 'Char'. To go the other way, see 'charToSpace'-spaceToChar :: HorizontalSpace -> Char-spaceToChar Space = ' '-spaceToChar Tab = '\t'---- | Try to turn a 'Char' into a Space. To go the other way, see 'spaceToChar'-charToSpace :: Char -> Maybe HorizontalSpace-charToSpace c = case c of- ' ' -> Just Space- '\t' -> Just Tab- _ -> Nothing---- | Parse 'Text' into 'Spaces', or turn spaces into 'Data.Text.Text'-spacesText :: Prism' Text Spaces-spacesText =- prism'- (Text.pack . foldMap (pure . spaceToChar))- (fmap V.fromList . traverse charToSpace . Text.unpack)---- | Parse 'String' into 'Spaces', or convert 'Spaces' into 'String'-spacesString :: Prism' String Spaces-spacesString =- prism'- (fmap spaceToChar . V.toList)- (fmap V.fromList . traverse charToSpace)---- | 'Spaced' is a value with zero or many horizontal spaces around it on--- both sides.-data Spaced a =- Spaced {- _before :: Spaces- , _after :: Spaces- , _value :: a- }- deriving (Eq, Ord, Show, Generic)--instance NFData a => NFData (Spaced a)---- | Classy lenses for 'Spaced'-class HasSpaced s t a b | s -> a, t -> b, s b -> t, t a -> s where- spaced :: Lens s t (Spaced a) (Spaced b)- after :: (s ~ t) => Lens s t Spaces Spaces- {-# INLINE after #-}- before :: (s ~ t) => Lens s t Spaces Spaces- {-# INLINE before #-}- spacedValue :: Lens s t a b- {-# INLINE spacedValue #-}- default after :: (s ~ t, a ~ b) => Lens s t Spaces Spaces- after = spaced . after- default before :: (s ~ t, a ~ b) => Lens s t Spaces Spaces- before = spaced . before- default spacedValue :: (s ~ t, a ~ b) => Lens s t a b- spacedValue = spaced . spacedValue--instance HasSpaced (Spaced a) (Spaced b) a b where- {-# INLINE after #-}- {-# INLINE before #-}- {-# INLINE spacedValue #-}- spaced = id- before f (Spaced x y z) = fmap (\w -> Spaced w y z) (f x)- spacedValue f (Spaced x y z) = fmap (Spaced x y) (f z)- after f (Spaced x y z) = fmap (\w -> Spaced x w z) (f y)--instance Functor Spaced where- fmap f (Spaced b t a) = Spaced b t (f a)---- | Appends the right parameter on the inside of the left parameter------ > Spaced " " () " " *> Spaced "\t\t\t" () "\t \t" == Spaced " \t\t\t" () "\t \t "-instance Applicative Spaced where- pure = unspaced- Spaced b t f <*> Spaced b' t' a = Spaced (b <> b') (t' <> t) (f a)--instance Foldable Spaced where- foldMap f = f . _value--instance Traversable Spaced where- traverse f (Spaced b t a) = fmap (Spaced b t) (f a)---- | 'betwixt' is just the constructor for 'Spaced' with a different--- argument order, which is sometimes useful.-betwixt :: Spaces -> a -> Spaces -> Spaced a-betwixt b a t = Spaced b t a---- | Places its argument in a 'Spaced' with no spaces.-unspaced :: a -> Spaced a-unspaced = uniform mempty---- | 'uniform' puts the same spacing both before and after something.-uniform :: Spaces -> a -> Spaced a-uniform s a = Spaced s s a---- | Remove spaces from the argument-removeSpaces :: Spaced a -> Spaced a-removeSpaces = unspaced . _value
sv.cabal view
@@ -1,5 +1,5 @@ name: sv-version: 0.1+version: 1.0 license: BSD3 license-file: LICENCE author: George Wilson@@ -18,21 +18,17 @@ sv uses an Applicative combinator style for decoding and encoding, rather than a type class based approach. This means we can have multiple decoders for the same type, multiple combinators of the same type, and we never have- to worry about orphan instances. These decoders can be stiched together from+ to worry about orphan instances. These decoders can be stitched together from provided primitives and combinators, or you can build one from a parser from your favourite parser combinator library. .+ For parsing, sv uses <https://hackage.haskell.org/package/hw-dsv hw-dsv>, a high performance streaming CSV parser based on rank-select data structures.+ sv works with UTF-8, and should work with CP-1252 as well. It does not work+ with UTF-16 or UTF-32.+ . sv returns values for all errors that occur - not just the first. Errors have more structure than just a string, indicating what went wrong. .- sv's parser is exposed so you can use it independently of the decoding, and- encoding and printing are similarly standalone.- .- sv focuses on correctness, on flexible and composable data types,- and on useful and informative error values.- Speed is also important to us, but it is not as important as these other- qualities.- . sv tries not to be opinionated about how your data should look. We intend for the user to have a great degree of freedom to build the right decoder for their dataset.@@ -51,9 +47,24 @@ * Encoding data to a CSV: <https://github.com/qfpl/sv/blob/master/examples/src/Data/Sv/Example/Encoding.hs Encoding.hs> * Handling NULL and Unknown occuring in a column of numbers: <https://github.com/qfpl/sv/blob/master/examples/src/Data/Sv/Example/Numbers.hs Numbers.hs> * Dealing with non-rectangular data: <https://github.com/qfpl/sv/blob/master/examples/src/Data/Sv/Example/Ragged.hs Ragged.hs>- * Handling multiple logical documents in one file: <https://github.com/qfpl/sv/blob/master/examples/src/Data/Sv/Example/Concat.hs Concat.hs> * Integrating with an existing attoparsec parser to read date stamps: <https://github.com/qfpl/sv/blob/master/examples/src/Data/Sv/Example/TableTennis.hs TableTennis.hs>- * Fixing inconsistent formatting with lenses: <https://github.com/qfpl/sv/blob/master/examples/src/Data/Sv/Example/Requote.hs Requote.hs>+ .+ To get the best performance, the hw-dsv parser and its dependencies+ underlying sv should be compiled with the flag @+bmi2@ to enable . These+ libraries are: @bits-extra@, @hw-rankselect@, @hw-rankselect-base@, and+ @hw-dsv@. A simple way to set the flags for all of them when building with+ cabal is to include a cabal.project file in your project containing+ something like the following:+ .+ >packages: .+ >package bits-extra+ > flags: +bmi2+ >package hw-rankselect+ > flags: +bmi2+ >package hw-rankselect-base+ > flags: +bmi2+ >package hw-dsv+ > flags: +bmi2 homepage: https://github.com/qfpl/sv bug-reports: https://github.com/qfpl/sv/issues@@ -63,7 +74,8 @@ tested-with: GHC == 7.10.3 , GHC == 8.0.2 , GHC == 8.2.2- , GHC == 8.4.1+ , GHC == 8.4.3+ , GHC == 8.6.1 source-repository head type: git@@ -72,56 +84,26 @@ library exposed-modules: Data.Sv , Data.Sv.Decode- , Data.Sv.Decode.Error- , Data.Sv.Decode.Type , Data.Sv.Encode- , Data.Sv.Encode.Options- , Data.Sv.Encode.Type , Data.Sv.Parse- , Data.Sv.Parse.Internal- , Data.Sv.Parse.Options- , Data.Sv.Print- , Data.Sv.Print.Internal- , Data.Sv.Print.Options- , Data.Sv.Syntax- , Data.Sv.Syntax.Field- , Data.Sv.Syntax.Record- , Data.Sv.Syntax.Sv- , Data.Vector.NonEmpty- , Text.Escape- , Text.Newline- , Text.Quote- , Text.Space- -- other-modules:+ , Data.Sv.Structure+ other-modules: Data.Sv.Alien.Cassava -- other-extensions: - build-depends: ansi-wl-pprint >= 0.6.6 && < 0.7+ build-depends: base >=4.8 && <5 , attoparsec >= 0.12.1.4 && < 0.14- , base >=4.8 && <5 , bifunctors >= 5.1 && < 6 , bytestring >= 0.9.1.10 && < 0.11- , charset >=0.3 && <=0.4- , containers >= 0.4 && < 0.6- , contravariant >= 1.2 && < 1.5- , deepseq >= 1.1 && < 1.5- , lens >= 4 && < 5- , mtl >= 2.0.1 && < 2.3- , parsec >= 3.1 && < 3.2- , parsers >=0.12 && <0.13- , profunctors >= 5.2.1 && < 6- , readable >= 0.3 && < 0.4+ , contravariant >= 1.2 && < 1.6+ , hw-dsv >= 0.2.1 && < 0.3 , semigroupoids >= 5 && <6- , semigroups >= 0.18 && < 0.19- , text >= 1.0 && < 1.3+ , sv-core >= 0.1 && < 0.2 , transformers >= 0.2 && < 0.6- , trifecta >= 1.5 && < 1.8 , utf8-string >= 1 && < 1.1 , validation >= 1 && < 1.1- , vector >= 0.10 && < 0.13- , void >= 0.6 && < 1 hs-source-dirs: src default-language: Haskell2010 ghc-options:- -Wall+ -Wall -O2 test-suite tasty type:@@ -129,30 +111,29 @@ main-is: tasty.hs other-modules:- Data.Sv.DecodeTest+ Data.Sv.CassavaTest+ , Data.Sv.DecodeTest , Data.Sv.EncodeTest- , Data.Sv.Generators- , Data.Sv.ParseTest- , Data.Sv.PrintTest- , Data.Sv.RoundTrips+ , Data.Sv.RoundTripsDecodeEncode default-language: Haskell2010 build-depends:- ansi-wl-pprint >= 0.6.6 && < 0.7- , base >=4.8 && <5+ base >=4.8 && <5 , bytestring >= 0.9.1.10 && < 0.11- , contravariant >= 1.2 && < 1.5- , hedgehog >= 0.5 && < 0.6+ , cassava >= 0.4.1 && < 0.6+ , contravariant >= 1.2 && < 1.6+ , hedgehog >= 0.5 && < 0.7 , lens >= 4 && < 5 , parsers >=0.12 && <0.13+ , Only >= 0.1 && < 0.2 , semigroupoids >= 5 && <6 , semigroups >= 0.18 && < 0.19 , sv- , tasty >= 0.11 && < 1.1- , tasty-hedgehog >= 0.1 && < 0.2+ , tasty >= 0.11 && < 1.2+ , tasty-hedgehog >= 0.1 && < 0.3 , tasty-hunit >= 0.9 && < 0.11 , text >= 1.0 && < 1.3- , trifecta >= 1.5 && < 1.8+ , trifecta >= 1.5 && < 2.1 , utf8-string >= 1 && < 1.1 , validation >= 1 && < 1.1 , vector >= 0.10 && < 0.13
+ test/Data/Sv/CassavaTest.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE ScopedTypeVariables #-}++module Data.Sv.CassavaTest (test_CassavaAgreement) where++import qualified Data.ByteString as BS+import qualified Data.Csv as Csv+import Data.Sv (Validation (Failure, Success), Headedness (Unheaded), ParseOptions (_headedness), defaultParseOptions)+import qualified Data.Sv as Sv (parseDecode)+import Data.Sv.Decode (Decode')+import qualified Data.Sv.Decode as Sv+import Data.Vector as V+import Data.Tuple.Only (Only (Only, fromOnly))+import Hedgehog (Gen, (===), failure, forAll, property)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range+import Test.Tasty (TestName, TestTree, testGroup)+import Test.Tasty.Hedgehog (testProperty)++test_CassavaAgreement :: TestTree+test_CassavaAgreement =+ testGroup "cassava agreement"+ [ cassavaAgreement "int" Sv.int (Gen.int (Range.linear (-10000000) 10000000))+ , cassavaAgreement "char" Sv.char (Gen.unicode)+ , cassavaAgreement "integer" Sv.integer (Gen.integral (Range.linear (-10000000) 10000000))+ , cassavaAgreement "string" Sv.string (Gen.string (Range.linear 1 500) Gen.unicode)+ , cassavaAgreement "bytestring" Sv.byteString (Gen.utf8 (Range.linear 1 500) Gen.unicode)+ , cassavaAgreement "float" Sv.float (Gen.float (Range.exponentialFloat (-10000000) 10000000))+ , cassavaAgreement "double" Sv.double (Gen.double (Range.exponentialFloat (-10000000) 10000000))+ ]++opts :: ParseOptions+opts = defaultParseOptions { _headedness = Unheaded }++-- | Test that decoding with sv gets the same result as decoding with cassava+cassavaAgreement :: forall a . (Csv.FromField a, Csv.ToField a, Show a, Eq a) => TestName -> Decode' BS.ByteString a -> Gen a -> TestTree+cassavaAgreement name dec gen = testProperty name $ property $ do+ a <- forAll gen+ let oa = Only a+ let sa = Csv.encode [oa]+ let cassava :: Either String [a]+ cassava = fmap fromOnly . V.toList <$> Csv.decode Csv.NoHeader sa+ sv = Sv.parseDecode dec opts sa+ case cassava of+ Left _ -> failure+ Right csa -> case sv of+ Failure _ -> failure+ Success sva -> do+ csa === sva+ sva === pure a
test/Data/Sv/DecodeTest.hs view
@@ -6,14 +6,18 @@ module Data.Sv.DecodeTest (test_Decode) where import Control.Applicative (liftA2)-import Control.Lens ((&), (.~))-import Data.ByteString-import Data.Functor.Alt+import Data.ByteString (ByteString)+import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString.Char8 as BS8+import Data.Functor.Alt ((<!>)) import Data.List.NonEmpty (NonEmpty ((:|)))-import Data.Semigroup+import Text.Read (readMaybe)+import Data.Semigroup (Semigroup)+import Data.Semigroupoid (Semigroupoid (o)) import qualified Data.Vector as V import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase, (@?=))+import Data.Text import Data.Sv import qualified Data.Sv.Decode as D@@ -23,30 +27,31 @@ testGroup "Decode" [ intOrStringTest , varyingLengthTest+ , semigroupoidTest ] data IntOrString = I Int | S String deriving (Eq, Ord, Show) -intOrString :: Decode ByteString ByteString IntOrString+intOrString :: D.Decode' ByteString IntOrString intOrString = I <$> D.int <!> S <$> D.string data V3 a = V3 a a a deriving (Eq, Ord, Show, Functor, Foldable, Traversable) -v3 :: Semigroup e => Decode e s a -> Decode e s (V3 a)+v3 :: Semigroup e => D.Decode e s a -> D.Decode e s (V3 a) v3 f = sequenceA (V3 f f f) -v3ios :: Decode ByteString ByteString (V3 IntOrString)+v3ios :: D.Decode ByteString ByteString (V3 IntOrString) v3ios = v3 intOrString -csv1 :: ByteString-csv1 = intercalate "\r\n" [- "\"3\", \"4\", \"5\""- , "\"quoted text\", unquoted text, 100"- , "7, unquoted text, 5"+csv1 :: LBS.ByteString+csv1 = LBS.intercalate "\n" [+ "\"3\",\"4\",\"5\""+ , "\"quoted text\",unquoted text,100"+ , "7,unquoted text,5" ] csv1' :: [V3 IntOrString]@@ -56,16 +61,16 @@ , V3 (I 7) (S "unquoted text") (I 5) ] -opts :: ParseOptions ByteString-opts = defaultParseOptions & headedness .~ Unheaded+opts :: ParseOptions+opts = ParseOptions comma Unheaded intOrStringTest :: TestTree intOrStringTest = testCase "parse successfully" $ parseDecode v3ios opts csv1 @?= pure csv1' -varyingLength :: ByteString-varyingLength = intercalate "\r\n" [+varyingLength :: LBS.ByteString+varyingLength = LBS.intercalate "\r\n" [ "one" , "one,two" , "one,two,three"@@ -73,15 +78,46 @@ , "one,two,three,four,five" ] -str2 :: Decode' ByteString (ByteString, ByteString)+str2 :: D.Decode' ByteString (ByteString, ByteString) str2 = liftA2 (,) D.contents D.contents varyingLengthTest :: TestTree varyingLengthTest = testCase "varyingLength has all the right errors" $ parseDecode str2 opts varyingLength @?=- Failure (DecodeErrors (D.UnexpectedEndOfRow :| [- D.ExpectedEndOfRow (V.fromList $ fmap pure [Unquoted "three"])- , D.ExpectedEndOfRow (V.fromList $ fmap pure [Unquoted "three", Unquoted "four"])- , D.ExpectedEndOfRow (V.fromList $ fmap pure [Unquoted "three", Unquoted "four", Unquoted "five"])+ Failure (DecodeErrors (UnexpectedEndOfRow :| [+ ExpectedEndOfRow (V.fromList ["three"])+ , ExpectedEndOfRow (V.fromList ["three", "four"])+ , ExpectedEndOfRow (V.fromList ["three", "four", "five"]) ]))++semiTestString1 :: LBS.ByteString+semiTestString1 = "hello,5,6.6,goodbye"++semiTestString2 :: LBS.ByteString+semiTestString2 = "hello,no,6.6,goodbye"++semiTestString3 :: LBS.ByteString+semiTestString3 = "hello,5,false,goodbye"++parseDecoder :: D.Decode' ByteString Int+parseDecoder = D.contents D.>>==+ \bs -> validateMaybe (D.BadDecode bs) . readMaybe . BS8.unpack $ bs++data Semi = Semi Text Int Double Text deriving (Eq, Show)++semiD :: D.Decode' ByteString Semi+semiD = Semi <$> D.utf8 <*> (parseDecoder `o` D.contents) <*> D.double <*> D.utf8++semigroupoidTest :: TestTree+semigroupoidTest = testGroup "Semigroupoid Decode"+ [ testCase "Does the right thing in the case of success" $+ parseDecode semiD opts semiTestString1 @?=+ pure [Semi "hello" 5 6.6 "goodbye"]+ , testCase "Does the right thing in the case of left failure" $+ parseDecode semiD opts semiTestString2 @?=+ Failure (DecodeErrors (pure (BadDecode "no")))+ , testCase "Does the right thing in the case of right failure" $+ parseDecode semiD opts semiTestString3 @?=+ Failure (DecodeErrors (pure (BadDecode "Couldn't parse \"false\" as a double")))+ ]
test/Data/Sv/EncodeTest.hs view
@@ -49,9 +49,9 @@ decidableTests = testGroup "decidable" [ testCase "encode an Int" $- encodeRow intOrString opts i @?= "\"5\""+ encodeRow intOrString opts i @?= "5" , testCase "encode a String" $- encodeRow intOrString opts s @?= "\"hello\""+ encodeRow intOrString opts s @?= "hello" ] intEmptyAndString :: Encode IntAndString@@ -64,16 +64,16 @@ divisibleTests = testGroup "divisible" [ testCase "encode an IntAndString" $- encodeRow intAndString opts ias @?= "\"10\",\"goodbye\""+ encodeRow intAndString opts ias @?= "10,goodbye" , testCase "encode an IntAndString with an empty between" $- encodeRow intEmptyAndString opts ias @?= "\"10\",\"\",\"goodbye\""+ encodeRow intEmptyAndString opts ias @?= "10,,goodbye" ] encodeTests :: TestTree encodeTests = testCase "multiple lines" $ encode (divided intAndString intOrString) opts [(IAS 3 "book", I 4), (IAS 7 "film", S "ok")]- @?= "\"3\",\"book\",\"4\"\r\n\"7\",\"film\",\"ok\""+ @?= "3,book,4\n7,film,ok" escapeTests :: TestTree escapeTests =
− test/Data/Sv/Generators.hs
@@ -1,115 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Data.Sv.Generators (- genSv- , genSvWithHeadedness- , genNewline- , genSep- , genQuote- , genSpaced- , genField- , genSpacedField- , genRecord- , genRecords- , genHeader- , genCsvString-) where--import Control.Applicative ((<$>), liftA2, liftA3)-import Data.ByteString (ByteString)-import qualified Data.ByteString.Lazy as LBS-import Data.ByteString.Builder (Builder)-import qualified Data.ByteString.Builder as Builder-import Data.List.NonEmpty (NonEmpty ((:|)))-import Data.Semigroup (Semigroup ((<>)))-import qualified Data.Vector as V-import Hedgehog-import qualified Hedgehog.Gen as Gen-import qualified Hedgehog.Range as Range--import Data.Sv.Syntax.Sv (Sv (Sv), Header (Header), Headedness, getHeadedness, Separator)-import Data.Sv.Syntax.Field (Field (Quoted, Unquoted))-import Data.Sv.Syntax.Record (Record, Records (EmptyRecords, Records), recordNel)-import Text.Escape (Unescaped (Unescaped))-import Text.Newline (Newline (CRLF, LF))-import Text.Space (Spaces, Spaced (Spaced))-import Text.Quote (Quote (SingleQuote, DoubleQuote))--genSv :: Gen Separator -> Gen Spaces -> Gen s -> Gen (Sv s)-genSv sep spc s =- let rs = genRecords spc s- e = Gen.list (Range.linear 0 5) genNewline- h = Gen.maybe (genHeader spc s genNewline)- in Sv <$> sep <*> h <*> rs <*> e--genSvWithHeadedness :: Gen Separator -> Gen Spaces -> Gen s -> Gen (Sv s, Headedness)-genSvWithHeadedness sep spc s = fmap (\c -> (c, getHeadedness c)) (genSv sep spc s)--genNewline :: Gen Newline-genNewline =- -- TODO put CR back in- Gen.element [CRLF, LF]--genSep :: Gen Separator-genSep =- Gen.element ['|', ',', '\t', '\x1F4A9']--genSpaced :: Gen Spaces -> Gen s -> Gen (Spaced s)-genSpaced spc str =- liftA3 Spaced spc spc str--genQuote :: Gen Quote-genQuote =- Gen.element [SingleQuote, DoubleQuote]--genUnescaped :: Gen a -> Gen (Unescaped a)-genUnescaped = fmap Unescaped--genField :: Gen s -> Gen (Field s)-genField s =- Gen.choice [- Unquoted <$> s- , liftA2 Quoted genQuote (genUnescaped s)- ]--genSpacedField :: Gen Spaces -> Gen s -> Gen (Spaced (Field s))-genSpacedField spc s = genSpaced spc (genField s)--genRecord :: Gen Spaces -> Gen s -> Gen (Record s)-genRecord spc s =- recordNel <$> Gen.nonEmpty (Range.linear 1 10) (genSpacedField spc s)--genHeader :: Gen Spaces -> Gen s -> Gen Newline -> Gen (Header s)-genHeader spc s n =- Header <$> genRecord spc s <*> n--genRecords :: Gen Spaces -> Gen s -> Gen (Records s)-genRecords spc s =- let rec = genRecord spc s- nlr = liftA2 (,) genNewline rec- in maybe EmptyRecords (uncurry Records) <$>- Gen.maybe (- liftA2 (,)- rec- (V.fromList <$> Gen.list (Range.linear 0 1000) nlr)- )--genCsvString :: Gen ByteString-genCsvString =- let intercalate' :: (Semigroup m, Monoid m) => m -> NonEmpty m -> m- intercalate' _ (x:|[]) = x- intercalate' m (x:|y:zs) = x <> m <> intercalate' m (y:|zs)- genNewlineString :: Gen Builder- genNewlineString = Gen.element (fmap Builder.string7 ["\n", "\r", "\r\n"])- genCsvRowString = intercalate' "," <$> Gen.nonEmpty (Range.linear 1 100) genCsvField- enquote c s = fmap (\z -> c <> z <> c) s- genCsvFieldString :: Gen Builder- genCsvFieldString = Builder.byteString <$>- Gen.utf8 (Range.linear 1 50) (Gen.filter (`notElem` [',','"','\'','\n','\r']) Gen.unicode)- genCsvField =- Gen.choice [- enquote "\"" genCsvFieldString- , enquote "'" genCsvFieldString- , genCsvFieldString- ]- in fmap (LBS.toStrict . Builder.toLazyByteString) $ intercalate' <$> genNewlineString <*> Gen.nonEmpty (Range.linear 0 100) genCsvRowString
− test/Data/Sv/ParseTest.hs
@@ -1,215 +0,0 @@-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE OverloadedStrings #-}--module Data.Sv.ParseTest (test_Parse) where--import Control.Lens ((&), (.~))-import Data.ByteString (ByteString)-import qualified Data.ByteString.UTF8 as UTF8-import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty)-import Data.Either (isLeft)-import Data.Foldable (fold)-import Data.Semigroup (Semigroup ((<>)))-import Data.Text (Text, pack)-import Hedgehog-import Test.Tasty (TestName, TestTree, testGroup)-import Test.Tasty.Hedgehog (testProperty)-import Test.Tasty.HUnit (Assertion, assertBool, testCase, (@?=))-import Text.Newline (Newline (CR, LF, CRLF), newlineToString)-import Text.Parser.Char (CharParsing)-import Text.Trifecta (Result (Success, Failure), parseByteString, _errDoc)--import Data.Sv.Generators (genCsvString)-import Data.Sv.Parse (ParseOptions, defaultParseOptions, headedness, separator, encodeString)-import Data.Sv.Parse.Internal (doubleQuotedField, record, separatedValues, singleQuotedField, spaced, spacedField)-import Data.Sv.Syntax.Sv (Sv, mkSv, comma, pipe, tab, Headedness (Unheaded), Separator)-import Data.Sv.Syntax.Field (Field (Quoted, Unquoted), SpacedField)-import Data.Sv.Syntax.Record (Record (Record), recordNel, mkRecords, Records (EmptyRecords))-import Text.Escape (Unescaped (Unescaped))-import Text.Space (Spaced (Spaced), manySpaces, unspaced)-import Text.Quote (Quote (SingleQuote, DoubleQuote), quoteToString)--test_Parse :: TestTree-test_Parse =- testGroup "Parse" [- singleQuotedFieldTest- , doubleQuotedFieldTest- , fieldTest- , recordTest- , csvTest- , psvTest- , tsvTest- , nsvTest- , crsvTest- , bssvTest- , randomCsvTest- ]--r2e :: Result a -> Either String a-r2e r = case r of- Success a -> Right a- Failure e -> Left (show (_errDoc e))--(@?=/) ::- (Show a, Show b, Eq a, Eq b)- => Either a b- -> b- -> Assertion-(@?=/) l r = l @?= pure r--qd, qs :: a -> Field a-qd = Quoted DoubleQuote . Unescaped-qs = Quoted SingleQuote . Unescaped-qsr :: s -> Record s-qsr = Record . pure . nospc . qs-uq :: s -> SpacedField s-uq = unspaced . Unquoted-uqa :: NonEmpty s -> Record s-uqa = recordNel . fmap uq-uqaa :: [NonEmpty s] -> [Record s]-uqaa = fmap uqa-nospc :: Field s -> SpacedField s-nospc = unspaced--quotedFieldTest :: (forall m . CharParsing m => m (SpacedField Text)) -> TestName -> Quote -> TestTree-quotedFieldTest parser name quote =- let p :: [ByteString] -> Either String (SpacedField Text)- p = r2e . parseByteString parser mempty . mconcat- q = quoteToString quote- qq = Quoted quote . Unescaped- in testGroup name [- testCase "empty" $- p [q,q] @?=/ nospc (qq "")- , testCase "text" $- p [q,"hello text",q]- @?=/ nospc (qq "hello text")- , testCase "capture space" $- p [" ", q, " spaced text ", q, " "]- @?=/ Spaced (manySpaces 3) (manySpaces 5) (qq " spaced text ")- , testCase "no closing quote" $- assertBool "wasn't left" (isLeft (p [q, "no closing quote" ]))- , testCase "no opening quote" $- assertBool "wasn't left" (isLeft (p [ "no opening quote", q]))- , testCase "no quotes" $- assertBool "wasn't left" (isLeft (p [ "no quotes" ]))- , testCase "quoted field can handle escaped quotes" $- p [q,"yes", q, q, "no", q] @?=/ nospc (Quoted quote (Unescaped ("yes" <> quoteToString quote <> "no")))- ]--singleQuotedFieldTest, doubleQuotedFieldTest :: TestTree-singleQuotedFieldTest = quotedFieldTest (spaced comma (singleQuotedField pack)) "singleQuotedField" SingleQuote-doubleQuotedFieldTest = quotedFieldTest (spaced comma (doubleQuotedField pack)) "doubleQuotedField" DoubleQuote--fieldTest :: TestTree-fieldTest =- let p :: ByteString -> Either String (SpacedField Text)- p = r2e . parseByteString (spacedField comma pack) mempty- in testGroup "field" [- testCase "doublequoted" $- p "\"hello\"" @?=/ nospc (qd "hello")- , testCase "singlequoted" $- p "'goodbye'" @?=/ nospc (qs "goodbye")- , testCase "unquoted" $- p "yes" @?=/ uq "yes"- , testCase "spaced doublequoted" $- p " \" spaces \" " @?=/ Spaced (manySpaces 7) (manySpaces 4) (qd " spaces ")- , testCase "spaced singlequoted" $- p " ' more spaces ' " @?=/ Spaced (manySpaces 8) (manySpaces 1) (qs " more spaces ")- , testCase "spaced unquoted" $- p " some text " @?=/ Spaced (manySpaces 2) (manySpaces 3) (Unquoted "some text")- , testCase "fields can include the separator in single quotes" $- p "'hello,there,'" @?=/ nospc (qs "hello,there,")- , testCase "fields can include the separator in double quotes" $- p "\"court,of,the,,,,crimson,king\"" @?=/ nospc (qd "court,of,the,,,,crimson,king")- , testCase "unquoted fields stop at the separator (1)" $- p "close,to,the,edge" @?=/ uq "close"- , testCase "unquoted fields stop at the separator (2)" $- p ",close,to,the,edge" @?=/ uq ""- ]--recordTest :: TestTree-recordTest =- let opts :: ParseOptions Text- opts = defaultParseOptions & encodeString .~ pack- p :: ByteString -> Either String (Record Text)- p = r2e . parseByteString (record opts) mempty- in testGroup "record" [- testCase "single field" $- p "Yes" @?=/ uqa (pure "Yes")- , testCase "fields" $- p "Anderson,Squire,Wakeman,Howe,Bruford" @?=/ uqa ("Anderson":|["Squire", "Wakeman", "Howe", "Bruford"])- , testCase "commas" $- p ",,," @?=/ uqa ("":|["","",""])- , testCase "record ends at newline" $- p "a,b,c\nd,e,f" @?=/ uqa ("a":|["b","c"])- , testCase "record ends at carriage return" $- p "g,h,i\rj,k,l" @?=/ uqa ("g":|["h","i"])- , testCase "record ends at carriage return followed by newline" $- p "m,n,o\r\np,q,r" @?=/ uqa ("m":|["n","o"])- ]--separatedValuesTest :: Separator -> Newline -> Int -> TestTree-separatedValuesTest sep nl newlines =- let opts = defaultParseOptions & separator .~ sep & headedness .~ Unheaded & encodeString .~ pack- p :: ByteString -> Either String (Sv Text)- p = r2e . parseByteString (separatedValues opts) mempty- ps = p . fold- mkSv' :: [Record s] -> [Newline] -> Sv s- mkSv' rs e = mkSv sep Nothing e $ maybe EmptyRecords (mkRecords nl) $ nonEmpty rs- s = UTF8.fromString [sep]- nls = newlineToString nl- terminator = replicate newlines nl- termStr = foldMap newlineToString terminator- in testGroup "separatedValues" [- testCase "empty" $- ps ["", termStr] @?=/ mkSv' [] terminator- , testCase "single empty quotes field" $ - ps ["''", termStr] @?=/ mkSv' [qsr ""] terminator- , testCase "single field, single record" $- ps ["one", termStr] @?=/ mkSv' [uqa (pure "one")] terminator- , testCase "single field, multiple records" $- ps ["one",nls,"un",termStr] @?=/ mkSv' [uqa (pure "one"), uqa (pure "un")] terminator- , testCase "multiple fields, single record" $- ps ["one", s, "two",termStr] @?=/ mkSv' (uqaa (pure ("one":|["two"]))) terminator- , testCase "multiple fields, multiple records" $- ps ["one", s, "two", s, "three", nls, "un", s, "deux", s, "trois",termStr]- @?=/ mkSv' (uqaa ["one":|["two", "three"] , "un":|["deux", "trois"]]) terminator- ]--svTest :: String -> Separator -> TestTree-svTest name sep =- testGroup name $ separatedValuesTest sep <$> [CR, LF, CRLF] <*> [0,1,2]--csvTest :: TestTree-csvTest = svTest "csv" comma--psvTest :: TestTree-psvTest = svTest "psv" pipe--tsvTest :: TestTree-tsvTest = svTest "tsv" tab--nsvTest :: TestTree-nsvTest = svTest "NULL separated values" '\0'--crsvTest :: TestTree-crsvTest =- testGroup "carriage return separated values" $- separatedValuesTest '\r' <$> [LF] <*> [0,1,2]--bssvTest :: TestTree-bssvTest = svTest "backspace separated values" '\BS'--prop_randomCsvTest :: Property-prop_randomCsvTest = property $ do- str <- forAll genCsvString- let opts = separatedValues (defaultParseOptions & headedness .~ Unheaded & encodeString .~ id)- x :: Either String (Sv String)- x = r2e (parseByteString opts mempty str)- case x of- Left _ -> failure- Right _ -> success--randomCsvTest :: TestTree-randomCsvTest =- testProperty "parse random CSV" prop_randomCsvTest
− test/Data/Sv/PrintTest.hs
@@ -1,32 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Data.Sv.PrintTest (test_Print) where--import Data.ByteString (ByteString)-import Test.Tasty (TestTree, testGroup)-import Test.Tasty.HUnit ((@?=), testCase)--import Data.Sv.Print (printSv)-import Data.Sv.Syntax.Field (Field (Quoted))-import Data.Sv.Syntax.Record (Records (EmptyRecords), singleField, singleRecord)-import Data.Sv.Syntax.Sv (Sv (Sv), noHeader, comma)-import Text.Quote (Quote (SingleQuote))--test_Print :: TestTree-test_Print =- testGroup "Print" [- csvPrint- ]--csvPrint :: TestTree-csvPrint =- testGroup "csvPrint" [- testCase "empty" $- let subject :: Sv ByteString- subject = Sv comma noHeader EmptyRecords []- in printSv subject @?= ""- , testCase "empty quotes" $- let subject :: Sv ByteString- subject = Sv comma noHeader (singleRecord (singleField (Quoted SingleQuote mempty))) []- in printSv subject @?= ("''" :: ByteString)- ]
− test/Data/Sv/RoundTrips.hs
@@ -1,190 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}--module Data.Sv.RoundTrips (test_Roundtrips) where--import Control.Lens ((&), (.~))-import Data.ByteString (ByteString)-import qualified Data.ByteString.Builder as Builder-import qualified Data.ByteString.Lazy as BL-import qualified Data.ByteString.UTF8 as UTF8-import Data.Semigroup ((<>))-import Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Vector as V-import Hedgehog ((===), Property, Gen, forAll, property)-import qualified Hedgehog.Gen as Gen-import qualified Hedgehog.Range as Range-import Test.Tasty (TestName, TestTree, testGroup)-import Test.Tasty.Hedgehog (testProperty)-import Test.Tasty.HUnit ((@?=), testCase)-import Text.Parser.Char (CharParsing)-import Text.Trifecta (parseByteString)--import Data.Sv-import qualified Data.Sv.Decode as D-import qualified Data.Sv.Encode as E-import Data.Sv.Decode.Error (trifectaResultToEither)-import Data.Sv.Generators-import Data.Sv.Syntax (Sv, Headedness, SpacedField, comma)-import Data.Sv.Parse (defaultParseOptions, headedness, encodeString, separatedValues)-import Data.Sv.Parse.Internal (spacedField)-import Data.Sv.Print (defaultPrintOptions, printSvText)-import Data.Sv.Print.Internal (printSpaced)-import Text.Space (HorizontalSpace (Space, Tab), Spaces)- -test_Roundtrips :: TestTree-test_Roundtrips =- testGroup "Round trips" [- csvRoundTrip- , fieldRoundTrip- , testGroup "decode/encode isomorphisms" [- bool- , char- , int- , integer- , float- , double- , string- , byteString- , lazyByteString- , text- ]- , testGroup "decode/encode normalising" [- boolSI- , floatSI- , doubleSI- ]-- ]--printAfterParseRoundTrip :: (forall m. CharParsing m => m a) -> (a -> ByteString) -> TestName -> ByteString -> TestTree-printAfterParseRoundTrip parser display name s =- testCase name $- fmap display (trifectaResultToEither $ parseByteString parser mempty s) @?= Right s--fieldRoundTrip :: TestTree-fieldRoundTrip =- let sep = comma- test =- printAfterParseRoundTrip- (spacedField sep UTF8.fromString :: CharParsing m => m (SpacedField ByteString))- (BL.toStrict . Builder.toLazyByteString . printSpaced defaultPrintOptions)- in testGroup "field" [- test "empty" ""- , test "unquoted" "wobble"- , test "unquoted with space" " wiggle "- , test "single quoted" "'tortoise'"- , test "single quoted with space" " 'turtle' "- , test "single quoted with escape outer" "\'\'\'c\'\'\'"- , test "single quoted with escape in the middle" "\' The char \'\'c\'\' is nice.\'"- , test "double quoted" "\"honey badger\""- , test "double quoted with space" " \" sun bear\" "- , test "double quoted with escape outer" "\"\"\"laser\"\"\""- , test "double quoted with escape in the middle" "\"John \"\"The Duke\"\" Wayne\""- ]--csvRoundTrip :: TestTree-csvRoundTrip = testProperty "roundtrip" prop_csvRoundTrip--prop_csvRoundTrip :: Property-prop_csvRoundTrip =- let genSpace :: Gen HorizontalSpace- genSpace = Gen.element [Space, Tab]- genSpaces :: Gen Spaces- genSpaces = V.fromList <$> Gen.list (Range.linear 0 10) genSpace- genText :: Gen Text- genText = Gen.text (Range.linear 1 100) Gen.alphaNum- gen = genSvWithHeadedness (pure comma) genSpaces genText- mkOpts h = defaultParseOptions & headedness .~ h & encodeString .~ Text.pack- parseCsv :: CharParsing m => Headedness -> m (Sv Text)- parseCsv = separatedValues . mkOpts- parse h = parseByteString (parseCsv h) mempty- in property $ do- (c,h) <- forAll gen- trifectaResultToEither (fmap printSvText (parse h (printSvText c))) === pure (printSvText c)--encOpts :: EncodeOptions-encOpts = defaultEncodeOptions & quote .~ Nothing--parOpts :: ParseOptions ByteString-parOpts = defaultParseOptions & headedness .~ Unheaded---- Round-trips an encode/decode pair. This version checks whether the pair--- form an isomorphism-roundTripCodecIso :: (Eq a, Show a) => TestName -> Decode' ByteString a -> Encode a -> [(ByteString, a)] -> TestTree-roundTripCodecIso name dec enc bsas = testGroup name . flip foldMap bsas $ \(bs,a) ->- [ testCase (UTF8.toString bs <> ": encode . decode") $- Success (BL.fromStrict bs) @?= (encode enc encOpts <$> parseDecode dec parOpts bs)- , testCase (UTF8.toString bs <> ": decode . encode") $- Success [a] @?= (parseDecode dec parOpts $ BL.toStrict $ encodeRow enc encOpts a)- ]---- Round-trips an encode/decode pair. This version checks whether the pair--- form a split-idempotent. That is to say, one direction is identity, the other is--- idempotent.-roundTripCodecSplitIdempotent :: (Eq a, Show a) => TestName -> Decode' ByteString a -> Encode a -> [(ByteString, a)] -> TestTree-roundTripCodecSplitIdempotent name dec enc bsas =- let deco = parseDecode dec parOpts- enco = encode enc encOpts- encdec = fmap enco . deco- in testGroup name . flip foldMap bsas $ \(bs,a) ->- [ testCase (UTF8.toString bs <> ": decode . encode . decode") $- Success (Success [a]) @?= (deco . BL.toStrict <$> encdec bs)- , testCase (UTF8.toString bs <> ": decode . encode") $- Success [a] @?= (parseDecode dec parOpts $ BL.toStrict $ enco [a])- ]--byteString :: TestTree-byteString = roundTripCodecIso "bytestring" D.contents E.byteString- [ ("hello","hello")]--lazyByteString :: TestTree-lazyByteString = roundTripCodecIso "lazy bytestring" D.lazyByteString E.lazyByteString [("hello","hello")]--bool :: TestTree-bool = roundTripCodecIso "bool" D.boolean E.booltruefalse [("true", True), ("false", False)]--char :: TestTree-char = roundTripCodecIso "char" D.char E.char [(UTF8.fromString "c", 'c'), (UTF8.fromString "💩", '💩')]--string :: TestTree-string = roundTripCodecIso "string" D.string E.string [(UTF8.fromString "hello", "hello"), (UTF8.fromString "💩💩💩💩", "💩💩💩💩")]--int :: TestTree-int = roundTripCodecIso "int" D.int E.int [("5", 5)]--integer :: TestTree-integer = roundTripCodecIso "integer" D.integer E.integer- [ ("5", 5)- , ("1000000", 1000000)- ]--float :: TestTree-float = roundTripCodecIso "float" D.float E.float- [ ("5.0", 5)- , ("10.5", 10.5)- , ("12345.678", 12345.678)- ]--double :: TestTree-double = roundTripCodecIso "double" D.double E.double [("5.0", 5)]--text :: TestTree-text = roundTripCodecIso "text" D.utf8 E.text [(UTF8.fromString "hello", "hello"), (UTF8.fromString "💩💩💩💩", "💩💩💩💩")]--boolSI :: TestTree-boolSI = roundTripCodecSplitIdempotent "bool" D.boolean E.bool10- [ ("1", True)- , ("0", False)- ]--floatSI :: TestTree-floatSI = roundTripCodecSplitIdempotent "float" D.float E.float- [ ("5", 5)- ]--doubleSI :: TestTree-doubleSI = roundTripCodecSplitIdempotent "double" D.double E.double- [ ("5", 5)- ]
+ test/Data/Sv/RoundTripsDecodeEncode.hs view
@@ -0,0 +1,130 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}++module Data.Sv.RoundTripsDecodeEncode (test_Roundtrips) where++import Control.Lens ((&), (.~))+import Data.ByteString (ByteString)+import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString.UTF8 as UTF8+import Data.Semigroup ((<>))+import Test.Tasty (TestName, TestTree, testGroup)+import Test.Tasty.HUnit ((@?=), testCase)++import Data.Sv+import qualified Data.Sv.Decode as D+import qualified Data.Sv.Encode as E++test_Roundtrips :: TestTree+test_Roundtrips =+ testGroup "Round trips" [+ testGroup "decode/encode isomorphisms" [+ bool+ , char+ , int+ , integer+ , float+ , double+ , string+ , byteString+ , lazyByteString+ , text+ ]+ , testGroup "decode/encode normalising" [+ boolSI+ , floatSI+ , doubleSI+ ]++ ]++encOpts :: EncodeOptions+encOpts = defaultEncodeOptions & quoting .~ Never++parOpts :: ParseOptions+parOpts = defaultParseOptions & headedness .~ Unheaded++lbUtf8 :: LBS.ByteString -> String+lbUtf8 = UTF8.toString . LBS.toStrict++utf8lb :: String -> LBS.ByteString+utf8lb = LBS.fromStrict . UTF8.fromString++-- Round-trips an encode/decode pair. This version checks whether the pair+-- form an isomorphism+roundTripCodecIso :: (Eq a, Show a) => TestName -> Decode' ByteString a -> Encode a -> [(LBS.ByteString, a)] -> TestTree+roundTripCodecIso name dec enc bsas = testGroup name . flip foldMap bsas $ \(bs,a) ->+ [ testCase (lbUtf8 bs <> ": encode . decode") $+ Success bs @?= (encode enc encOpts <$> parseDecode dec parOpts bs)+ , testCase (lbUtf8 bs <> ": decode . encode") $+ Success [a] @?= (parseDecode dec parOpts $ encodeRow enc encOpts a)+ ]++-- Round-trips an encode/decode pair. This version checks whether the pair+-- form a split-idempotent. That is to say, one direction is identity, the other is+-- idempotent.+roundTripCodecSplitIdempotent :: (Eq a, Show a) => TestName -> Decode' ByteString a -> Encode a -> [(LBS.ByteString, a)] -> TestTree+roundTripCodecSplitIdempotent name dec enc bsas =+ let deco = parseDecode dec parOpts+ enco = encode enc encOpts+ encdec = fmap enco . deco+ in testGroup name . flip foldMap bsas $ \(bs,a) ->+ [ testCase (lbUtf8 bs <> ": decode . encode . decode") $+ Success (Success [a]) @?= (deco <$> encdec bs)+ , testCase (lbUtf8 bs <> ": decode . encode") $+ Success [a] @?= (parseDecode dec parOpts $ enco [a])+ ]++byteString :: TestTree+byteString = roundTripCodecIso "bytestring" D.contents E.byteString+ [ ("hello","hello")]++lazyByteString :: TestTree+lazyByteString = roundTripCodecIso "lazy bytestring" D.lazyByteString E.lazyByteString [("hello","hello")]++bool :: TestTree+bool = roundTripCodecIso "bool" D.boolean E.booltruefalse [("true", True), ("false", False)]++char :: TestTree+char = roundTripCodecIso "char" D.char E.char [(utf8lb "c", 'c'), (utf8lb "💩", '💩')]++string :: TestTree+string = roundTripCodecIso "string" D.string E.string [(utf8lb "hello", "hello"), (utf8lb "💩💩💩💩", "💩💩💩💩")]++int :: TestTree+int = roundTripCodecIso "int" D.int E.int [("5", 5)]++integer :: TestTree+integer = roundTripCodecIso "integer" D.integer E.integer+ [ ("5", 5)+ , ("1000000", 1000000)+ ]++float :: TestTree+float = roundTripCodecIso "float" D.float E.float+ [ ("5.0", 5)+ , ("10.5", 10.5)+ , ("12345.678", 12345.678)+ ]++double :: TestTree+double = roundTripCodecIso "double" D.double E.double [("5.0", 5)]++text :: TestTree+text = roundTripCodecIso "text" D.utf8 E.text [(utf8lb "hello", "hello"), (utf8lb "💩💩💩💩", "💩💩💩💩")]++boolSI :: TestTree+boolSI = roundTripCodecSplitIdempotent "bool" D.boolean E.bool10+ [ ("1", True)+ , ("0", False)+ ]++floatSI :: TestTree+floatSI = roundTripCodecSplitIdempotent "float" D.float E.float+ [ ("5", 5)+ ]++doubleSI :: TestTree+doubleSI = roundTripCodecSplitIdempotent "double" D.double E.double+ [ ("5", 5)+ ]
test/tasty.hs view
@@ -2,18 +2,16 @@ import Test.Tasty (defaultMain, testGroup) +import Data.Sv.CassavaTest (test_CassavaAgreement) import Data.Sv.DecodeTest (test_Decode) import Data.Sv.EncodeTest (test_Encode)-import Data.Sv.ParseTest (test_Parse)-import Data.Sv.PrintTest (test_Print)-import Data.Sv.RoundTrips (test_Roundtrips)+import Data.Sv.RoundTripsDecodeEncode (test_Roundtrips) main :: IO () main = defaultMain $ testGroup "Tests" [- test_Parse- , test_Print- , test_Decode+ test_Decode , test_Encode , test_Roundtrips+ , test_CassavaAgreement ]