packages feed

burrito-2.0.1.3: source/library/Burrito/Internal/Expand.hs

{-# OPTIONS_GHC -Wno-missing-export-lists #-}

module Burrito.Internal.Expand where

import qualified Burrito.Internal.Render as Render
import qualified Burrito.Internal.Type.Character as Character
import qualified Burrito.Internal.Type.Digit as Digit
import qualified Burrito.Internal.Type.Expression as Expression
import qualified Burrito.Internal.Type.Field as Field
import qualified Burrito.Internal.Type.Literal as Literal
import qualified Burrito.Internal.Type.MaxLength as MaxLength
import qualified Burrito.Internal.Type.Modifier as Modifier
import qualified Burrito.Internal.Type.Name as Name
import qualified Burrito.Internal.Type.Operator as Operator
import qualified Burrito.Internal.Type.Template as Template
import qualified Burrito.Internal.Type.Token as Token
import qualified Burrito.Internal.Type.Value as Value
import qualified Burrito.Internal.Type.Variable as Variable
import qualified Control.Monad.Trans.Class as Trans
import qualified Control.Monad.Trans.State as State
import qualified Data.ByteString as ByteString
import qualified Data.Char as Char
import qualified Data.Functor.Identity as Identity
import qualified Data.List as List
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Data.Text.Lazy as LazyText
import qualified Data.Text.Lazy.Builder as Builder

-- | Expands a template using the given values. Unlike parsing, expansion
-- always succeeds. If no value is given for a variable, it will simply not
-- appear in the output.
--
-- >>> expand [] <$> parse "valid-template"
-- Just "valid-template"
-- >>> expand [] <$> parse "template:{example}"
-- Just "template:"
-- >>> expand [("example", stringValue "true")] <$> parse "template:{example}"
-- Just "template:true"
expand :: [(String, Value.Value)] -> Template.Template -> String
expand values =
  let m = Map.mapKeys Text.pack $ Map.fromList values
   in Render.builderToString
        . Identity.runIdentity
        . expandWith
          (pure . flip Map.lookup m)

-- | This is like @expand@ except that it gives you more control over how
-- variables are expanded. If you can, use @expand@. It's simpler.
--
-- Instead of passing in a static mapping from names to
-- values, you pass in a function that is used to look up values on the fly.
-- This can be useful if computing values takes a while or requires some impure
-- actions.
--
-- >>> expandWith (\ x -> [Nothing, Just . stringValue $ unpack x]) <$> parse "template:{example}"
-- Just ["template:","template:example"]
-- >>> let Just template = parse "user={USER}"
-- >>> expandWith (fmap (fmap stringValue) . lookupEnv . unpack) template
-- "user=taylor"
--
-- Note that as the RFC specifies, the given function will be called at most
-- once for each variable in the template.
--
-- >>> let Just template = parse "{a}{a}"
-- >>> expandWith (\ x -> do { putStrLn $ "-- expanding " <> show x; pure . Just $ Burrito.stringValue "A" }) template
-- -- expanding "a"
-- "AA"
expandWith ::
  (Monad m) =>
  (Text.Text -> m (Maybe Value.Value)) ->
  Template.Template ->
  m Builder.Builder
expandWith f = flip State.evalStateT Map.empty . template (cached f)

type CacheT = State.StateT (Map.Map Text.Text (Maybe Value.Value))

cached ::
  (Monad m) =>
  (Text.Text -> m (Maybe Value.Value)) ->
  Name.Name ->
  CacheT m (Maybe Value.Value)
cached f x = do
  let key = LazyText.toStrict . Builder.toLazyText $ name x
  cache <- State.get
  case Map.lookup key cache of
    Just result -> pure result
    Nothing -> do
      result <- Trans.lift $ f key
      State.modify $ Map.insert key result
      pure result

template ::
  (Monad m) =>
  (Name.Name -> CacheT m (Maybe Value.Value)) ->
  Template.Template ->
  CacheT m Builder.Builder
template f = fmap mconcat . traverse (token f) . Template.tokens

token ::
  (Monad m) =>
  (Name.Name -> CacheT m (Maybe Value.Value)) ->
  Token.Token ->
  CacheT m Builder.Builder
token f x = case x of
  Token.Expression y -> expression f y
  Token.Literal y -> pure $ literal y

expression ::
  (Monad m) =>
  (Name.Name -> CacheT m (Maybe Value.Value)) ->
  Expression.Expression ->
  CacheT m Builder.Builder
expression f ex =
  let op = Expression.operator ex
   in fmap
        ( mconcat
            . (\xs -> if null xs then xs else prefix op : xs)
            . List.intersperse (separator op)
            . Maybe.catMaybes
        )
        . traverse (variable f op)
        . NonEmpty.toList
        $ Expression.variables ex

separator :: Operator.Operator -> Builder.Builder
separator op = Builder.singleton $ case op of
  Operator.Ampersand -> '&'
  Operator.FullStop -> '.'
  Operator.None -> ','
  Operator.NumberSign -> ','
  Operator.PlusSign -> ','
  Operator.QuestionMark -> '&'
  Operator.Semicolon -> ';'
  Operator.Solidus -> '/'

prefix :: Operator.Operator -> Builder.Builder
prefix op = case op of
  Operator.Ampersand -> Builder.singleton '&'
  Operator.FullStop -> Builder.singleton '.'
  Operator.None -> mempty
  Operator.NumberSign -> Builder.singleton '#'
  Operator.PlusSign -> mempty
  Operator.QuestionMark -> Builder.singleton '?'
  Operator.Semicolon -> Builder.singleton ';'
  Operator.Solidus -> Builder.singleton '/'

variable ::
  (Monad m) =>
  (Name.Name -> CacheT m (Maybe Value.Value)) ->
  Operator.Operator ->
  Variable.Variable ->
  CacheT m (Maybe Builder.Builder)
variable f op var = do
  res <- f $ Variable.name var
  pure $ case res of
    Nothing -> Nothing
    Just val -> value op var val

value ::
  Operator.Operator ->
  Variable.Variable ->
  Value.Value ->
  Maybe Builder.Builder
value op var val = case val of
  Value.Dictionary xs -> dictionaryValue op var $ Map.toAscList xs
  Value.List xs -> listValue op var xs
  Value.String x -> Just $ stringValue op var x

dictionaryValue ::
  Operator.Operator ->
  Variable.Variable ->
  [(Text.Text, Text.Text)] ->
  Maybe Builder.Builder
dictionaryValue = items $ \op var (k, v) ->
  let f = string op Modifier.None
   in case Variable.modifier var of
        Modifier.Asterisk -> [f k <> Builder.singleton '=' <> f v]
        _ -> [f k, f v]

listValue ::
  Operator.Operator ->
  Variable.Variable ->
  [Text.Text] ->
  Maybe Builder.Builder
listValue = items $ \op var ->
  pure
    . stringValue
      ( case Variable.modifier var of
          Modifier.Asterisk -> op
          _ -> Operator.None
      )
      var {Variable.modifier = Modifier.None}

items ::
  (Operator.Operator -> Variable.Variable -> a -> [Builder.Builder]) ->
  Operator.Operator ->
  Variable.Variable ->
  [a] ->
  Maybe Builder.Builder
items f op var xs =
  let md = Variable.modifier var
      sep = case md of
        Modifier.Asterisk -> separator op
        _ -> Builder.singleton ','
      p = case md of
        Modifier.Asterisk -> False
        _ -> case op of
          Operator.Ampersand -> True
          Operator.QuestionMark -> True
          Operator.Semicolon -> True
          _ -> False
   in if null xs
        then Nothing
        else
          Just
            . mconcat
            . (if p then (label True var :) else id)
            . List.intersperse sep
            $ concatMap (f op var) xs

label :: Bool -> Variable.Variable -> Builder.Builder
label p v =
  name (Variable.name v) <> if p then Builder.singleton '=' else mempty

name :: Name.Name -> Builder.Builder
name =
  mconcat
    . List.intersperse (Builder.singleton '.')
    . fmap field
    . NonEmpty.toList
    . Name.fields

field :: Field.Field -> Builder.Builder
field = foldMap (character $ const True) . Field.characters

character :: (Char -> Bool) -> Character.Character tag -> Builder.Builder
character f x = case x of
  Character.Encoded y z -> Render.encodedCharacter y z
  Character.Unencoded y -> unencodedCharacter f y

stringValue ::
  Operator.Operator -> Variable.Variable -> Text.Text -> Builder.Builder
stringValue op var str =
  let pre = case op of
        Operator.Ampersand -> label True var
        Operator.QuestionMark -> label True var
        Operator.Semicolon -> label (not $ Text.null str) var
        _ -> mempty
   in pre <> string op (Variable.modifier var) str

string ::
  Operator.Operator -> Modifier.Modifier -> Text.Text -> Builder.Builder
string op md =
  let allowed x = case op of
        Operator.NumberSign -> isAllowed x
        Operator.PlusSign -> isAllowed x
        _ -> isUnreserved x
      trim = case md of
        Modifier.Colon ml -> Text.take $ MaxLength.count ml
        _ -> id
   in foldMap (unencodedCharacter allowed) . Text.unpack . trim

isAllowed :: Char -> Bool
isAllowed x = isUnreserved x || isReserved x

isUnreserved :: Char -> Bool
isUnreserved x = case x of
  '-' -> True
  '.' -> True
  '_' -> True
  '~' -> True
  _ -> Char.isAsciiUpper x || Char.isAsciiLower x || Char.isDigit x

isReserved :: Char -> Bool
isReserved x = case x of
  '!' -> True
  '$' -> True
  '&' -> True
  '\'' -> True
  '(' -> True
  ')' -> True
  '*' -> True
  '+' -> True
  ',' -> True
  ';' -> True
  '=' -> True
  ':' -> True
  '/' -> True
  '?' -> True
  '#' -> True
  '[' -> True
  ']' -> True
  '@' -> True
  _ -> False

unencodedCharacter :: (Char -> Bool) -> Char -> Builder.Builder
unencodedCharacter f x =
  if f x
    then Builder.singleton x
    else foldMap (uncurry Render.encodedCharacter) $ encodeCharacter x

encodeCharacter :: Char -> [(Digit.Digit, Digit.Digit)]
encodeCharacter =
  fmap Digit.fromWord8 . ByteString.unpack . Text.encodeUtf8 . Text.singleton

literal :: Literal.Literal -> Builder.Builder
literal = foldMap (character isAllowed) . Literal.characters