packages feed

sexpr-0.2.0: Codec/Sexpr/Internal.hs

-- |A Sexpr is an S-expressionin the style of Rivest's Canonical
-- S-expressions.  Atoms may be of any type, but String and
-- ByteString have special support.  Rivest's implementation of
-- S-expressions is unusual in supporting MIME type hints for each
-- atom.  See http://people.csail.mit.edu/rivest/Sexp.txt

module Codec.Sexpr.Internal (-- * Basics
                             Sexpr,
                             isAtom,
                             isList,
                             atom,
                             list,
                             unAtom,
                             unList,
                             -- * Hinted Atoms
                             hintedAtom,
                             hint,
                             defaultHint,
                             -- * Character predicates to support encoding
                             isTokenChar,isInitialTokenChar,isQuoteableChar,
                             -- * Transformations
                             fold
                            ) where

import Control.Applicative
import Data.Char
import qualified Data.Foldable as F
import Data.Traversable
import Test.QuickCheck
import Data.Monoid()

data Sexpr s = Atom s
             | HintedAtom String s
             | List [Sexpr s] 

instance Eq s => Eq (Sexpr s) where
    (List a) == (List b) = and $ zipWith (==) a b
    a == b = unAtom a == unAtom b && hint a == hint b

-- |The 'Functor' instance maps over the underlying atomic contents of
-- the S-expression.  It does not map only over the top-level list,
-- but over the fringe.
instance Functor Sexpr where
    fmap f (Atom s) = Atom (f s)
    fmap f (HintedAtom h s) = HintedAtom h (f s)
    fmap f (List ss) = List $ map (fmap f) ss

instance F.Foldable Sexpr where
    foldMap f (Atom s) = f s
    foldMap f (HintedAtom _ s) = f s
    foldMap f (List ss) = F.foldMap (F.foldMap f) ss

instance Traversable Sexpr where
    traverse f (Atom s) = Atom <$> f s
    traverse f (HintedAtom h s) = HintedAtom h <$> f s
    traverse f (List ss) = List <$> traverse (traverse f) ss

instance Arbitrary a => Arbitrary (Sexpr a) where
    arbitrary = sized arbSexpr
        where 
          arbSexpr 0 = oneof [Atom <$> arbitrary,
                              return $ List []]
          arbSexpr n = oneof [Atom <$> arbitrary,
                              List <$> (resize (n `div` 2) arbitrary)]
    coarbitrary (Atom s) = variant 0 . coarbitrary s
    coarbitrary (HintedAtom h s) = variant 1 . coarbitrary_h . coarbitrary s
        where coarbitrary_h = 
                foldr (\a b -> variant (ord a) . variant 1 . b) (variant 0) h
    coarbitrary (List ss) = variant 2 . coarbitrary ss

-- |@fold f s@ applies f to each sub-S-expression of s, from each leaf
-- to the root.  @f@ need not preserve the shape of @s@, in contrast
-- to the shape-preserving @Traversable@ instance.
fold :: (Sexpr t -> Sexpr t) -> Sexpr t -> Sexpr t
fold f z@(Atom _) = f z
fold f z@(HintedAtom _ _) = f z
fold f   (List ss) = f . List $ map (fold f) ss

-- |Any atom whose hint is not specified is assumed to be 
-- "text/plain; charset=iso-8859-1".  This is that default value.
defaultHint :: String
defaultHint = "text/plain; charset=iso-8859-1"

-- |Construct an atom.
atom :: a -> Sexpr a
atom s = Atom s

-- |Construct a list.
list :: [Sexpr a] -> Sexpr a
list xs = List xs

-- |Construct an atom with a MIME type hint.
-- @'hintedAtom' 'defaultHint' == 'atom'@
hintedAtom :: String -> a -> Sexpr a
hintedAtom h s | h == defaultHint = Atom s
hintedAtom h s = HintedAtom h s

-- |A predicate for recognizing lists.
isList :: Sexpr a -> Bool
isList (List _) = True
isList _ = False

-- |A predicate for identifying atoms, whether or not they have
-- explicit hints.
isAtom :: Sexpr a -> Bool
isAtom (List _) = False
isAtom _ = True

-- |Extract the hint of an atom.  Lists do not have hints, but all
-- atoms have hints.
hint :: Sexpr a -> Maybe String
hint (Atom _) = Just defaultHint
hint (HintedAtom h _) = Just h
hint _ = Nothing

-- |Extract the content of an atom, discarding any MIME type hint.
unAtom :: Sexpr s -> s
unAtom (Atom s) = s
unAtom (HintedAtom _ s) = s
unAtom _ = error "unAtom called on a non-atom"

-- |Extract the sub-S-expressions of a List.  If all you intend to do
-- is traverse or map over that list, the Functor instance of
-- S-expressions may work just fine.
unList :: Sexpr s -> [Sexpr s]
unList (List xs) = xs
unList _ = error "unList called on a non-list"

-- |Tokens may begin with any alphabetic character or the characters
-- in @"-./_:*+="@ ;
isInitialTokenChar :: Char -> Bool
isInitialTokenChar x = (isAlpha x || x `elem` "-./_:*+=") && (128 > ord x)

-- |Tokens may internally contain any of the characters legitimate to
-- begin tokens, or any numeral.
isTokenChar :: Char -> Bool
isTokenChar x = (isAlphaNum x || x `elem` "-./_:*+=") && (128 > ord x)

-- |Only token characters and spaces don't need to be escaped when
-- shown in the "quoted" syntax.
isQuoteableChar :: Char -> Bool
isQuoteableChar x = isTokenChar x || isSpace x