packages feed

jsonpatch-0.1.0.0: src/Data/JSON/Pointer.hs

-- <https://datatracker.ietf.org/doc/html/rfc6901/>
module Data.JSON.Pointer
  ( Pointer (..)
  , pointerFromText
  , pointerToText
  , pointerToString
  ) where

import Prelude

import Data.Aeson (FromJSON (..), withText)
import Data.Attoparsec.Text
import Data.JSON.Pointer.Token
import Data.List.NonEmpty (nonEmpty)
import Data.List.NonEmpty qualified as NE
import Data.Text (Text, unpack)
import Data.Text qualified as T

data Pointer
  = -- | @""@ means whole-document
    PointerEmpty
  | -- | @"/[.../]x"@ means path to a key or index @x@
    --
    -- NB. @"/"@ naturally becomes @'PointerPath' [] ('K' "")@
    PointerPath [Token] Token
  | -- | @"/[.../]-"@ means path to last element of an array
    PointerPathEnd [Token]
  deriving stock (Eq, Show)

instance FromJSON Pointer where
  parseJSON = withText "Pointer" $ either fail pure . pointerFromText

pointerFromText :: Text -> Either String Pointer
pointerFromText = \case
  "" -> Right PointerEmpty
  "/" -> Right $ PointerPath [] $ K ""
  "/-" -> Right $ PointerPathEnd []
  p -> case T.stripSuffix "/-" p of
    Nothing -> parseOnly pointerPathP p
    Just p' -> parseOnly pointerPathEndP p'

pointerToText :: Pointer -> Text
pointerToText = \case
  PointerEmpty -> ""
  PointerPath ts t -> tokensToText $ ts <> [t]
  PointerPathEnd ts -> tokensToText ts <> "/-"

pointerToString :: Pointer -> String
pointerToString = unpack . pointerToText

pointerPathP :: Parser Pointer
pointerPathP = do
  ts <- maybe (fail "") pure . nonEmpty =<< pointerP
  pure $ PointerPath (NE.init ts) $ NE.last ts

pointerPathEndP :: Parser Pointer
pointerPathEndP = PointerPathEnd <$> pointerP

pointerP :: Parser [Token]
pointerP = char '/' *> tokenP `sepBy1` char '/' <* endOfInput