packages feed

svfactor (empty) → 0.1

raw patch · 27 files changed

+2426/−0 lines, 27 filesdep +attoparsecdep +basedep +bifunctorssetup-changed

Dependencies added: attoparsec, base, bifunctors, bytestring, charset, deepseq, hedgehog, lens, parsec, parsers, semigroupoids, semigroups, svfactor, tasty, tasty-hedgehog, tasty-hunit, text, transformers, trifecta, utf8-string, vector

Files

+ LICENCE view
@@ -0,0 +1,31 @@+Copyright (c) 2017-2018, Commonwealth Scientific and Industrial Research Organisation+(CSIRO) ABN 41 687 119 230.++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 QFPL 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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ changelog.md view
@@ -0,0 +1,5 @@+# Revision history for svfactor++## 0.1 -- 2018-07-19++* First version. Released on an unsuspecting world.
+ src/Data/Svfactor.hs view
@@ -0,0 +1,18 @@+{-|+Module      : Data.Svfactor+Copyright   : (C) CSIRO 2017-2018+License     : BSD3+Maintainer  : George Wilson <george.wilson@data61.csiro.au>+Stability   : experimental+Portability : non-portable+-}++module Data.Svfactor (+  module Data.Svfactor.Parse+, module Data.Svfactor.Print+, module Data.Svfactor.Syntax+) where++import Data.Svfactor.Parse+import Data.Svfactor.Print+import Data.Svfactor.Syntax
+ src/Data/Svfactor/Parse.hs view
@@ -0,0 +1,123 @@+{-|+Module      : Data.Svfactor.Parse+Copyright   : (C) CSIRO 2017-2018+License     : BSD3+Maintainer  : George Wilson <george.wilson@data61.csiro.au>+Stability   : experimental+Portability : non-portable+-}++module Data.Svfactor.Parse (+  parseSv+, parseSv'+, parseSvFromFile+, parseSvFromFile'+, separatedValues++, module Data.Svfactor.Parse.Options+, SvParser (..)+, trifecta+, trifectaResultToEither+, attoparsecByteString+, attoparsecText+) where++import Control.Applicative ((<$>))+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.Monoid (mempty)+import Data.Text (Text)+import qualified Data.Text.IO as Text+import qualified Text.Trifecta as Trifecta++import Data.Svfactor.Syntax.Sv (Sv)+import Data.Svfactor.Parse.Internal (separatedValues)+import Data.Svfactor.Parse.Options++-- | Parse a 'ByteString' as an Sv.+--+-- This version uses Trifecta, hence it assumes its input is UTF-8 encoded.+parseSv :: ParseOptions ByteString -> ByteString -> Either ByteString (Sv ByteString)+parseSv = parseSv' trifecta++-- | 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++-- | 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++-- | 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)++-- | 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.+--+-- 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 $ Trifecta.parseByteString (separatedValues opts) mempty bs+  , runSvParserFromFile = \opts fp -> trifectaResultToEither <$> Trifecta.parseFromFileEx (separatedValues opts) fp+  }++-- | 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+  }++-- | 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+  }++-- | 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+
+ src/Data/Svfactor/Parse/Internal.hs view
@@ -0,0 +1,170 @@+{-|+Module      : Data.Svfactor.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 svfactor's parser.+As the Internal name suggests, this file is exempt from the PVP. Depend+on this module at your own risk!+-}++module Data.Svfactor.Parse.Internal (+  separatedValues+  , header+  , field+  , singleQuotedField+  , doubleQuotedField+  , unquotedField+  , spaced+  , spacedField+  , record+  , records+  , ending+) where++import Control.Applicative (Applicative ((<*>), pure), (*>), (<*), (<$), 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.Svfactor.Syntax.Sv (Sv (Sv), Header, mkHeader, noHeader, Headedness (Unheaded, Headed), headedness, Separator)+import Data.Svfactor.Syntax.Field (Field (Unquoted, Quoted))+import Data.Svfactor.Syntax.Record (Record (Record), Records (Records, EmptyRecords))+import Data.Svfactor.Parse.Options (ParseOptions, separator, endOnBlankLine, encodeString)+import Data.Svfactor.Vector.NonEmpty as V+import Data.Svfactor.Text.Escape (Unescaped (Unescaped))+import Data.Svfactor.Text.Newline (Newline (CR, CRLF, LF))+import Data.Svfactor.Text.Space (HorizontalSpace (Space, Tab), Spaces, Spaced, betwixt)+import Data.Svfactor.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/Svfactor/Parse/Options.hs view
@@ -0,0 +1,97 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE DefaultSignatures #-}++{-|+Module      : Data.Svfactor.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.Svfactor.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.Svfactor.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/Svfactor/Print.hs view
@@ -0,0 +1,105 @@+{-|+Module      : Data.Svfactor.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.Svfactor.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.Svfactor.Encode"+-}++module Data.Svfactor.Print (+  printSv+, printSvLazy+, printSv'+, printSvLazy'+, printSvText+, printSvTextLazy+, writeSvToFile+, writeSvToHandle+, writeSvToFile'+, writeSvToHandle'+, module Data.Svfactor.Print.Options+) where++import Data.ByteString (ByteString)+import qualified Data.ByteString.Lazy as LBS+import Data.ByteString.Builder as BSB+import Data.Foldable (Foldable (foldMap))+import Data.Semigroup ((<>))+import Data.Text (Text)+import System.IO (BufferMode (BlockBuffering), Handle, hClose, hSetBinaryMode, hSetBuffering, openFile, IOMode (WriteMode))++import Data.Svfactor.Print.Options+import Data.Svfactor.Print.Internal+import Data.Svfactor.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/Svfactor/Print/Internal.hs view
@@ -0,0 +1,75 @@+{-|+Module      : Data.Svfactor.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.Svfactor.Print".+-}++module Data.Svfactor.Print.Internal (+  printNewline+  , printField+  , printSpaced+  , printRecord+  , printRecords+  , printHeader+) where++import Control.Lens (review, view)+import Data.Bifoldable (bifoldMap)+import Data.ByteString.Builder as BSB+import Data.Foldable (Foldable (foldMap))+import Data.Monoid (Monoid (mempty))+import Data.Semigroup ((<>))+import Data.Semigroup.Foldable (intercalate1)++import Data.Svfactor.Print.Options+import Data.Svfactor.Syntax.Field (Field (Quoted, Unquoted), SpacedField)+import Data.Svfactor.Syntax.Record (Record (Record), Records (Records, EmptyRecords))+import Data.Svfactor.Syntax.Sv (Header (Header), Separator)+import Data.Svfactor.Text.Newline+import Data.Svfactor.Text.Space (spaceToChar, Spaced (Spaced))+import Data.Svfactor.Text.Quote++-- | Convert a 'Newline' to a ByteString 'Builder'+printNewline :: Newline -> Builder+printNewline = BSB.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 (BSB.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 (BSB.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/Svfactor/Print/Options.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE FlexibleInstances #-}++{-|+Module      : Data.Svfactor.Print.Options+Copyright   : (C) CSIRO 2017-2018+License     : BSD3+Maintainer  : George Wilson <george.wilson@data61.csiro.au>+Stability   : experimental+Portability : non-portable+-}++module Data.Svfactor.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 LBS+import Data.ByteString.Builder (Builder)+import qualified Data.ByteString.Builder as Builder+import Data.Text (Text)+import qualified Data.Text.Encoding as Text++import Data.Svfactor.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 LBS.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/Svfactor/Structure/Headedness.hs view
@@ -0,0 +1,30 @@+{-|+Module      : Data.Svfactor.Structure.Headedness+Copyright   : (C) CSIRO 2017-2018+License     : BSD3+Maintainer  : George Wilson <george.wilson@data61.csiro.au>+Stability   : experimental+Portability : non-portable+-}++module Data.Svfactor.Structure.Headedness (+  Headedness (Headed, Unheaded)+, HasHeadedness (headedness)+) where++import Control.Lens (Lens')++-- | 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
+ src/Data/Svfactor/Syntax.hs view
@@ -0,0 +1,18 @@+{-|+Module      : Data.Svfactor.Syntax+Copyright   : (C) CSIRO 2017-2018+License     : BSD3+Maintainer  : George Wilson <george.wilson@data61.csiro.au>+Stability   : experimental+Portability : non-portable+-}++module Data.Svfactor.Syntax (+  module Data.Svfactor.Syntax.Field+, module Data.Svfactor.Syntax.Record+, module Data.Svfactor.Syntax.Sv+) where++import Data.Svfactor.Syntax.Field+import Data.Svfactor.Syntax.Record+import Data.Svfactor.Syntax.Sv
+ src/Data/Svfactor/Syntax/Field.hs view
@@ -0,0 +1,117 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE DeriveGeneric #-}++{-|+Module      : Data.Svfactor.Syntax.Field+Copyright   : (C) CSIRO 2017-2018+License     : BSD3+Maintainer  : George Wilson <george.wilson@data61.csiro.au>+Stability   : experimental+Portability : non-portable+-}++module Data.Svfactor.Syntax.Field (+    Field (Unquoted, Quoted)+  , SpacedField+  , Spaced (Spaced)+  , HasFields (fields)+  , AsField (_Field, _Unquoted, _Quoted)+  , unescapedField+  , foldField+  , fieldContents+) where++import Control.Applicative ((<$>))+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 Data.Svfactor.Text.Escape (Unescaped (Unescaped, getRawUnescaped))+import Data.Svfactor.Text.Quote (Quote)+import Data.Svfactor.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/Svfactor/Syntax/Record.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE DeriveGeneric #-}++{-|+Module      : Data.Svfactor.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.Svfactor.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.Applicative (Applicative (..), (<$>))+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.Monoid (Monoid (..))+import Data.Semigroup (Semigroup)+import Data.Traversable (Traversable (traverse))+import Data.Vector (Vector)+import qualified Data.Vector as V+import GHC.Generics (Generic)++import Data.Svfactor.Syntax.Field (SpacedField, Field (Unquoted), HasFields (fields))+import Data.Svfactor.Vector.NonEmpty (NonEmptyVector)+import qualified Data.Svfactor.Vector.NonEmpty as V+import Data.Svfactor.Text.Newline (Newline)+import Data.Svfactor.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/Svfactor/Syntax/Sv.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE FlexibleInstances#-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveFoldable #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE DeriveGeneric #-}++{-|+Module      : Data.Svfactor.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.++You can program against it using the provided functions and optics.+For an example of this see+<https://github.com/qfpl/svfactor/blob/master/examples/src/Data/Svfactor/Example/Requote.hs Requote.hs>+-}++module Data.Svfactor.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.Applicative (Applicative ((<*>), pure), (<$>))+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.Svfactor.Structure.Headedness (Headedness (Headed, Unheaded), HasHeadedness (headedness))+import Data.Svfactor.Syntax.Field (HasFields (fields))+import Data.Svfactor.Syntax.Record (Record, Records (EmptyRecords), HasRecord (record), HasRecords (records, traverseNewlines, traverseRecords), recordList)+import Data.Svfactor.Text.Newline (Newline)+import Data.Svfactor.Text.Separator (Separator, HasSeparator (separator), comma, pipe, tab)++-- | '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)++instance HasSeparator (Sv s) where+  separator f (Sv x1 x2 x3 x4) =+    fmap (\y -> Sv y x2 x3 x4) (f x1)+  {-# INLINE separator #-}
+ src/Data/Svfactor/Text/Escape.hs view
@@ -0,0 +1,124 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveFoldable #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE DeriveGeneric #-}++{-|+Module      : Data.Svfactor.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 Data.Svfactor.Text.Escape (+  Unescaped (Unescaped, getRawUnescaped)+  , Escaper+  , Escaper'+  , escapeString+  , escapeText+  , escapeUtf8+  , escapeUtf8Lazy+  , escapeChar+) where++import Control.DeepSeq (NFData)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as LBS+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' BS.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' LBS.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/Data/Svfactor/Text/Newline.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Module      : Data.Svfactor.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 Data.Svfactor.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/Data/Svfactor/Text/Quote.hs view
@@ -0,0 +1,73 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeFamilies #-}++{-|+Module      : Data.Svfactor.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 Data.Svfactor.Text.Quote (+  Quote (SingleQuote, DoubleQuote)+  , AsQuote (_Quote, _SingleQuote, _DoubleQuote)+  , quoteChar+  , quoteToString+) where++import Control.Applicative (pure)+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/Data/Svfactor/Text/Separator.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE DeriveGeneric #-}++{-|+Module      : Data.Svfactor.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 Data.Svfactor.Text.Separator+  ( Separator+  , HasSeparator (..)+  , comma, pipe, tab+  )+where++import Control.Lens (Lens')++-- | 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 #-}++-- | 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/Svfactor/Text/Space.hs view
@@ -0,0 +1,194 @@+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE DeriveGeneric #-}++{-|+Module      : Data.Svfactor.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 Data.Svfactor.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.Applicative (Applicative (..))+import Control.DeepSeq (NFData (rnf))+import Control.Lens (Lens, Prism', prism, prism')+import Data.Foldable (Foldable (..))+import Data.Monoid (mempty)+import Data.Semigroup (Semigroup ((<>)))+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Traversable (Traversable (..))+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
+ src/Data/Svfactor/Vector/NonEmpty.hs view
@@ -0,0 +1,87 @@+{-# 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.Svfactor.Vector.NonEmpty (+  NonEmptyVector (NonEmptyVector)+, fromNel+, toNel+, toVector+, headNev+, tailNev+) where++import Control.Applicative (Applicative (..), (<$>))+import Control.DeepSeq (NFData)+import Control.Lens (Lens', lens)+import Data.Functor.Apply (Apply((<.>)))+import Data.Foldable (Foldable (..), toList)+import Data.List.NonEmpty (NonEmpty ((:|)))+import Data.Monoid (mappend)+import Data.Traversable (Traversable (..))+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++-- | Convert a 'NonEmptyVector' back to a 'Vector'+toVector :: NonEmptyVector a -> Vector a+toVector (NonEmptyVector a as) = V.cons a 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)
+ svfactor.cabal view
@@ -0,0 +1,123 @@+name:                svfactor+version:             0.1+license:             BSD3+license-file:        LICENCE+author:              George Wilson+maintainer:          george@qfpl.io+copyright:           Copyright (c) 2018, Commonwealth Scientific and Industrial Research Organisation (CSIRO) ABN 41 687 119 230.+category:            CSV, Text, Web+synopsis:+  Syntax-preserving CSV manipulation++description:+  <<http://i.imgur.com/uZnp9ke.png>>+  .+  svfactor is a library for parsing, manipulating, and printing CSV and+  similar formats (such as PSV, TSV, and many more).+  .+  svfactor retains all syntactic information including newlines, and+  provides lenses, prisms, and traversals to query and manipulate this+  structure.+  This should make it useful for writing custom CSV transformations and+  sanitisation tools. For example, one could easily use it to rewrite all+  CRLF newlines to LF.+  It also extends RFC4180 by allowing optional spacing surrounding fields.+  .+  svfactor's parser is exposed so you can use it independently and printing is similarly standalone.+  .+  Please note that there are __known performance problems with svfactor__.+  .+  Examples:+  .+  * Handling multiple logical documents in one file: <https://github.com/qfpl/svfactor/blob/master/examples/src/Data/Svfactor/Example/Concat.hs Concat.hs>+  * Fixing inconsistent formatting with lenses: <https://github.com/qfpl/svfactor/blob/master/examples/src/Data/Svfactor/Example/Requote.hs Requote.hs>++homepage:            https://github.com/qfpl/svfactor+bug-reports:         https://github.com/qfpl/svfactor/issues+build-type:          Simple+extra-source-files:  changelog.md+cabal-version:       >=1.10+tested-with:         GHC == 7.8.4+                     , GHC == 7.10.3+                     , GHC == 8.0.2+                     , GHC == 8.2.2+                     , GHC == 8.4.3++source-repository    head+  type:              git+  location:          git@github.com/qfpl/svfactor.git++library+  exposed-modules:     Data.Svfactor+                       , Data.Svfactor.Parse+                       , Data.Svfactor.Parse.Internal+                       , Data.Svfactor.Parse.Options+                       , Data.Svfactor.Print+                       , Data.Svfactor.Print.Internal+                       , Data.Svfactor.Print.Options+                       , Data.Svfactor.Syntax+                       , Data.Svfactor.Syntax.Field+                       , Data.Svfactor.Syntax.Record+                       , Data.Svfactor.Syntax.Sv+                       , Data.Svfactor.Text.Escape+                       , Data.Svfactor.Text.Newline+                       , Data.Svfactor.Text.Space+                       , Data.Svfactor.Text.Quote+                       , Data.Svfactor.Text.Separator+                       , Data.Svfactor.Structure.Headedness+                       , Data.Svfactor.Vector.NonEmpty++  -- other-modules:+  -- other-extensions:    +  build-depends:       attoparsec >= 0.12.1.4 && < 0.14+                       , base >=4.7 && <5+                       , bifunctors >= 5.1 && < 6+                       , bytestring >= 0.9.1.10 && < 0.11+                       , charset >=0.3 && <=0.4+                       , deepseq >= 1.1 && < 1.5+                       , lens >= 4 && < 5+                       , parsec >= 3.1 && < 3.2+                       , parsers >=0.12 && <0.13+                       , semigroupoids >= 5 && <6+                       , semigroups >= 0.18 && < 0.19+                       , text >= 1.0 && < 1.3+                       , transformers >= 0.2 && < 0.6+                       , trifecta >= 1.5 && < 2.1+                       , utf8-string >= 1 && < 1.1+                       , vector >= 0.10 && < 0.13+  hs-source-dirs:      src+  default-language:    Haskell2010+  ghc-options:+                       -Wall -O2++test-suite             tasty+  type:+                       exitcode-stdio-1.0+  main-is:+                       tasty.hs+  other-modules:+                       Data.Svfactor.Generators+                       , Data.Svfactor.ParseTest+                       , Data.Svfactor.PrintTest+                       , Data.Svfactor.RoundTripsParsePrint+  default-language:+                       Haskell2010+  build-depends:+                       base >=4.7 && <5+                       , bytestring >= 0.9.1.10 && < 0.11+                       , hedgehog >= 0.5 && < 0.7+                       , lens >= 4 && < 5+                       , parsers >=0.12 && <0.13+                       , semigroups >= 0.18 && < 0.19+                       , svfactor+                       , 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 && < 2.1+                       , utf8-string >= 1 && < 1.1+                       , vector >= 0.10 && < 0.13+  ghc-options:+                       -Wall+  hs-source-dirs:+                       test
+ test/Data/Svfactor/Generators.hs view
@@ -0,0 +1,115 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.Svfactor.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 BSB+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.Svfactor.Syntax.Sv (Sv (Sv), Header (Header), Headedness, getHeadedness, Separator)+import Data.Svfactor.Syntax.Field (Field (Quoted, Unquoted))+import Data.Svfactor.Syntax.Record (Record, Records (EmptyRecords, Records), recordNel)+import Data.Svfactor.Text.Escape (Unescaped (Unescaped))+import Data.Svfactor.Text.Newline (Newline (CRLF, LF))+import Data.Svfactor.Text.Space (Spaces, Spaced (Spaced))+import Data.Svfactor.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 BSB.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 = BSB.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 . BSB.toLazyByteString) $ intercalate' <$> genNewlineString <*> Gen.nonEmpty (Range.linear 0 100) genCsvRowString
+ test/Data/Svfactor/ParseTest.hs view
@@ -0,0 +1,215 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE OverloadedStrings #-}++module Data.Svfactor.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.Parser.Char (CharParsing)+import Text.Trifecta (Result (Success, Failure), parseByteString, _errDoc)++import Data.Svfactor.Generators (genCsvString)+import Data.Svfactor.Parse (ParseOptions, defaultParseOptions, headedness, separator, encodeString)+import Data.Svfactor.Parse.Internal (doubleQuotedField, record, separatedValues, singleQuotedField, spaced, spacedField)+import Data.Svfactor.Syntax.Sv (Sv, mkSv, comma, pipe, tab, Headedness (Unheaded), Separator)+import Data.Svfactor.Syntax.Field (Field (Quoted, Unquoted), SpacedField)+import Data.Svfactor.Syntax.Record (Record (Record), recordNel, mkRecords, Records (EmptyRecords))+import Data.Svfactor.Text.Escape (Unescaped (Unescaped))+import Data.Svfactor.Text.Newline (Newline (CR, LF, CRLF), newlineToString)+import Data.Svfactor.Text.Space (Spaced (Spaced), manySpaces, unspaced)+import Data.Svfactor.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/Svfactor/PrintTest.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.Svfactor.PrintTest (test_Print) where++import Data.ByteString (ByteString)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit ((@?=), testCase)++import Data.Svfactor.Print (printSv)+import Data.Svfactor.Syntax.Field (Field (Quoted))+import Data.Svfactor.Syntax.Record (Records (EmptyRecords), singleField, singleRecord)+import Data.Svfactor.Syntax.Sv (Sv (Sv), noHeader, comma)+import Data.Svfactor.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/Svfactor/RoundTripsParsePrint.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}++module Data.Svfactor.RoundTripsParsePrint (test_Roundtrips) where++import Control.Lens ((&), (.~))+import Data.ByteString (ByteString)+import qualified Data.ByteString.Builder as BSB+import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString.UTF8 as UTF8+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.Svfactor.Parse (trifectaResultToEither)+import Data.Svfactor.Generators+import Data.Svfactor.Syntax (Sv, Headedness, SpacedField, comma)+import Data.Svfactor.Parse (defaultParseOptions, headedness, encodeString, separatedValues)+import Data.Svfactor.Parse.Internal (spacedField)+import Data.Svfactor.Print (defaultPrintOptions, printSvText)+import Data.Svfactor.Print.Internal (printSpaced)+import Data.Svfactor.Text.Space (HorizontalSpace (Space, Tab), Spaces)+ +test_Roundtrips :: TestTree+test_Roundtrips =+  testGroup "Round trips" [+    csvRoundTrip+  , fieldRoundTrip+  ]++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))+        (LBS.toStrict . BSB.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)
+ test/tasty.hs view
@@ -0,0 +1,15 @@+module Main (main) where++import Test.Tasty (defaultMain, testGroup)++import Data.Svfactor.ParseTest (test_Parse)+import Data.Svfactor.PrintTest (test_Print)+import Data.Svfactor.RoundTripsParsePrint (test_Roundtrips)++main :: IO ()+main =+  defaultMain $ testGroup "Tests" [+    test_Parse+  , test_Print+  , test_Roundtrips+  ]