{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- | An orphan lives in this module, -- -- @ -- instance ('Functor' f, 'IsString' a) => 'IsString' ('Free' f a) -- @ -- -- so that we can write @\"simple queries\" :: 'Simple'@. module Language.Google.Search.Simple where import Prelude import Control.Monad.Free import Data.Char (isSpace) import Data.Monoid import Data.String (IsString (..)) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Lazy.Builder (Builder) import qualified Data.Text.Lazy.Builder as B instance (Functor f, IsString a) => IsString (Free f a) where fromString = return . fromString ------------------------------------------------------------------------ -- * Units data Duration = Days | Months | Years deriving (Show) data Size = Bytes | KBytes | MBytes deriving (Show) ------------------------------------------------------------------------ -- * Search expression construction -- | 'Builder' with precedence, though ambiguous associativity. -- (But that's okay because Google doesn't mind which way you lean.) -- -- Note that at Google @OR@ binds tighter than conjunction, which is flipped -- in contrast to everywhere else. We take the analogous Haskell fixities -- when building search expressions: -- -- * 11: atomic tokens or parenthesised expressions -- -- * 10: complementation, search operators (cf. Haskell prefix application) -- -- * 3: disjunction (@OR@ or @|@) -- -- * 2: conjunction (by juxtaposition) data PrecBuilder = PrecBuilder Int Builder deriving (Show) -- | Give me the precedence of the piece of syntax you're building, and I'll -- give you a function that parenthesise any sub-expressions when necessary. parentheses :: Int -> ((PrecBuilder -> Builder) -> Builder) -> PrecBuilder parentheses outer f = PrecBuilder outer . f $ \ (PrecBuilder inner e) -> if outer > inner then "(" <> e <> ")" else e -- | Render a search expression using Google search syntax. class SearchBuilder e where searchBuilder :: e -> PrecBuilder -- | Higher-order version of 'SearchBuilder'. class SyntaxBuilder f where syntaxBuilder :: f PrecBuilder -> PrecBuilder -- | 'SearchBuilder' for 'Free'! instance (Functor f, SearchBuilder a, SyntaxBuilder f) => SearchBuilder (Free f a) where searchBuilder = iter syntaxBuilder . fmap searchBuilder ------------------------------------------------------------------------ -- * Generalised Boolean operators infixr 2 \/ class DisjunctF f where disjunctF :: e -> e -> f e class Disjunct e where (\/) :: e -> e -> e instance (DisjunctF f) => Disjunct (Free f a) where a \/ b = Free (disjunctF a b) infixr 3 /\ class ConjunctF f where conjunctF :: e -> e -> f e class Conjunct e where (/\) :: e -> e -> e instance (ConjunctF f) => Conjunct (Free f a) where a /\ b = Free (conjunctF a b) class ComplementF f where complementF :: e -> f e class Complement e where notB :: e -> e instance (ComplementF f) => Complement (Free f a) where notB = Free . complementF -- | 'andB' is to '/\' what 'and' is to '&&'. andB :: (Conjunct e) => [e] -> e andB = foldr1 (/\) -- | 'orB' is to '\/' what 'or' is to '||'. orB :: (Disjunct e) => [e] -> e orB = foldr1 (\/) ------------------------------------------------------------------------ -- * Primitive Terms -- | 'Fuzzy' terms are grouped with parentheses (if necessary), while -- 'Exact' terms are always “double-quoted”. The 'IsString' instance -- defaults to 'Fuzzy', so just writing @\"literal string\" ∷ 'Term' 'Text'@ -- is acceptable. data Term t = Fuzzy t | Exact t deriving (Functor, Show) instance (IsString t) => IsString (Term t) where fromString = Fuzzy . fromString instance SearchBuilder Text where searchBuilder t = PrecBuilder prec (B.fromText t) where prec = if T.any isSpace t then 2 else 11 instance SyntaxBuilder Term where syntaxBuilder term = case term of Fuzzy e -> e Exact (PrecBuilder _ e) -> PrecBuilder 11 $ "\"" <> e <> "\"" instance (SearchBuilder a) => SearchBuilder (Term a) where searchBuilder = syntaxBuilder . fmap searchBuilder ------------------------------------------------------------------------ -- * Boolean expressions -- | The shape of Boolean expressions. infixr 3 `AndB` infixr 2 `OrB` data BooleanF e = NotB e | e `AndB` e | e `OrB` e deriving (Functor, Show) instance ConjunctF BooleanF where conjunctF = AndB instance DisjunctF BooleanF where disjunctF = OrB instance ComplementF BooleanF where complementF = NotB instance SyntaxBuilder BooleanF where syntaxBuilder bool = case bool of NotB e -> parentheses 10 $ \ p -> "-" <> p e a `AndB` b -> parentheses 2 $ \ p -> p a <> " " <> p b a `OrB` b -> parentheses 3 $ \ p -> p a <> " OR " <> p b -- | The free Boolean-shaped monad. No refunds. type BooleanM = Free BooleanF -- | Simple Boolean combinations of 'Term's. type Simple = BooleanM (Term Text)