packages feed

jsonlogic-hs-0.1.0.0: src/JsonLogic/Core/Location.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : JsonLogic.Core.Location
--
-- Where in a rule something is: a path from the root of the rule's JSON to
-- one node, written as a JSON Pointer (RFC 6901) when rendered.
--
-- In @{"if": [true, {"*": []}]}@ the @*@ is at @/if/1@: the key @if@, then
-- index 1 of its argument list. An operator written with a bare argument
-- instead of a list (@{"!": x}@) has that argument at @/!@. The root is the
-- empty pointer, @""@.
--
-- A JSON Pointer names a place in the rule's own JSON text, so a location is
-- meaningful to a tool in any language. Locations are attached to every
-- parse and evaluation error.
module JsonLogic.Core.Location
  ( Location
  , Segment (..)
  , root
  , child
  , fromSegments
  , locationSegments
  , renderLocation
  ) where

import Data.Text (Text)
import qualified Data.Text as Text

-- | One step from a JSON value into a part of it.
data Segment
  = -- | An object member, by key.
    Key !Text
  | -- | An array element, by zero-based index.
    Index !Int
  deriving (Eq, Ord, Show)

-- | A path from the root of a rule. Held innermost-first, so that stepping
-- into a child while reading a rule is constant time.
newtype Location = Location [Segment]
  deriving (Eq, Ord)

-- | Shown as the 'fromSegments' call that rebuilds it.
instance Show Location where
  showsPrec d location =
    showParen (d > 10) (showString "fromSegments " . showsPrec 11 (locationSegments location))

-- | The root of the rule.
root :: Location
root = Location []

-- | One step further in.
child :: Location -> Segment -> Location
child (Location reversed) segment = Location (segment : reversed)

-- | A location from its segments, outermost first.
fromSegments :: [Segment] -> Location
fromSegments = Location . reverse

-- | The segments of a location, outermost first.
locationSegments :: Location -> [Segment]
locationSegments (Location reversed) = reverse reversed

-- | The location as a JSON Pointer (RFC 6901): @""@ for the root,
-- otherwise @/@ before each segment, with @~@ written @~0@ and @/@ written
-- @~1@ inside keys.
renderLocation :: Location -> Text
renderLocation = foldMap (("/" <>) . renderSegment) . locationSegments
  where
    renderSegment (Key key) = Text.replace "/" "~1" (Text.replace "~" "~0" key)
    renderSegment (Index i) = Text.pack (show i)