|
| 1 | +{-# language BangPatterns #-} |
| 2 | +{-# language DuplicateRecordFields #-} |
| 3 | +{-# language NamedFieldPuns #-} |
| 4 | + |
| 5 | +-- | Flatten nested JSON objects into a single JSON object in which the keys |
| 6 | +-- have been joined by the separator. |
| 7 | +module Json.Flatten |
| 8 | + ( flatten |
| 9 | + ) where |
| 10 | + |
| 11 | +import Control.Monad.ST (ST) |
| 12 | +import Control.Monad.ST.Run (runByteArrayST) |
| 13 | +import Data.Builder.Catenable (Builder) |
| 14 | +import Data.ByteString.Short.Internal (ShortByteString(SBS)) |
| 15 | +import Data.Text.Short (ShortText) |
| 16 | +import Data.Primitive (SmallArray,ByteArray(ByteArray),MutableByteArray) |
| 17 | +import Data.Word (Word8) |
| 18 | +import Json (Member(Member)) |
| 19 | +import qualified Json |
| 20 | +import qualified Data.Chunks as Chunks |
| 21 | +import qualified Data.Primitive.Contiguous as C |
| 22 | +import qualified Data.Bytes as Bytes |
| 23 | +import qualified Data.Bytes.Text.Utf8 as Utf8 |
| 24 | +import qualified Data.Primitive as PM |
| 25 | +import qualified Data.Builder.Catenable as Builder |
| 26 | +import qualified Data.Text.Short as TS |
| 27 | +import qualified Data.Text.Short.Unsafe as TS |
| 28 | + |
| 29 | +-- | Flatten a json value, recursively descending into objects and joining |
| 30 | +-- keys with the separator. For example: |
| 31 | +-- |
| 32 | +-- > { "name": "bilbo" |
| 33 | +-- > , "occupation": |
| 34 | +-- > { "name": "burglar" |
| 35 | +-- > , "start": "2022-05-30" |
| 36 | +-- > } |
| 37 | +-- > , "height": 124 |
| 38 | +-- > , "favorites": ["adventures","lunch"] |
| 39 | +-- > } |
| 40 | +-- |
| 41 | +-- Becomes: |
| 42 | +-- |
| 43 | +-- > { "name": "bilbo" |
| 44 | +-- > , "occupation.name": "burglar" |
| 45 | +-- > , "occupation.start": "2022-05-30" |
| 46 | +-- > , "height": 124 |
| 47 | +-- > , "favorites": ["adventures","lunch"] |
| 48 | +-- > } |
| 49 | +-- |
| 50 | +-- Currently, the implementation of this function throws an exception if |
| 51 | +-- any separator other than period is used. This may be corrected in a future |
| 52 | +-- release. |
| 53 | +flatten :: Char -> Json.Value -> Json.Value |
| 54 | +flatten c v = case c of |
| 55 | + '.' -> flattenPeriod v |
| 56 | + _ -> errorWithoutStackTrace "Json.Flatten.flatten: only period is supported" |
| 57 | + |
| 58 | +-- built backwards |
| 59 | +data ShortTexts |
| 60 | + = ShortTextsCons !ShortText !ShortTexts |
| 61 | + | ShortTextsBase !ShortText |
| 62 | + |
| 63 | +flattenPeriod :: Json.Value -> Json.Value |
| 64 | +flattenPeriod x = case x of |
| 65 | + Json.Object mbrs -> |
| 66 | + let bldr = foldMap (\Member{key,value} -> flattenPrefix (ShortTextsBase key) value) mbrs |
| 67 | + chunks = Builder.run bldr |
| 68 | + result = Chunks.concat chunks |
| 69 | + in Json.Object result |
| 70 | + Json.Array ys -> Json.Array $! C.map' flattenPeriod ys |
| 71 | + _ -> x |
| 72 | + |
| 73 | +flattenPrefix :: |
| 74 | + ShortTexts -- context accumulator |
| 75 | + -> Json.Value |
| 76 | + -> Builder Json.Member |
| 77 | +flattenPrefix !pre x = case x of |
| 78 | + Json.Object mbrs -> flattenObject pre mbrs |
| 79 | + _ -> |
| 80 | + let !a = flattenPeriod x |
| 81 | + !k = runShortTexts pre |
| 82 | + !mbr = Json.Member{key=k,value=a} |
| 83 | + in Builder.Cons mbr Builder.Empty |
| 84 | + |
| 85 | +flattenObject :: ShortTexts -> SmallArray Json.Member -> Builder Json.Member |
| 86 | +flattenObject !pre !mbrs = foldMap |
| 87 | + (\Member{key,value} -> flattenPrefix (ShortTextsCons key pre) value |
| 88 | + ) mbrs |
| 89 | + |
| 90 | +runShortTexts :: ShortTexts -> ShortText |
| 91 | +runShortTexts !ts0 = go 0 ts0 |
| 92 | + where |
| 93 | + paste :: MutableByteArray s -> Int -> ShortTexts -> ST s ByteArray |
| 94 | + paste !dst !ix (ShortTextsBase t) = |
| 95 | + let len = Bytes.length (Utf8.fromShortText t) |
| 96 | + in case ix - len of |
| 97 | + 0 -> do |
| 98 | + PM.copyByteArray dst 0 (st2ba t) 0 len |
| 99 | + PM.unsafeFreezeByteArray dst |
| 100 | + _ -> errorWithoutStackTrace "Json.Flatten.runShortTexts: implementation mistake" |
| 101 | + paste !dst !ix (ShortTextsCons t ts) = do |
| 102 | + let !len = Bytes.length (Utf8.fromShortText t) |
| 103 | + let !ixNext = ix - len |
| 104 | + PM.copyByteArray dst ixNext (st2ba t) 0 len |
| 105 | + let !ixPred = ixNext - 1 |
| 106 | + PM.writeByteArray dst ixPred (0x2E :: Word8) |
| 107 | + paste dst ixPred ts |
| 108 | + go :: Int -> ShortTexts -> ShortText |
| 109 | + go !byteLenAcc (ShortTextsCons t ts) = |
| 110 | + go (Bytes.length (Utf8.fromShortText t) + byteLenAcc + 1) ts |
| 111 | + go !byteLenAcc (ShortTextsBase t) = |
| 112 | + let !(ByteArray r) = runByteArrayST $ do |
| 113 | + let totalLen = Bytes.length (Utf8.fromShortText t) + byteLenAcc |
| 114 | + dst <- PM.newByteArray totalLen |
| 115 | + paste dst totalLen ts0 |
| 116 | + in TS.fromShortByteStringUnsafe (SBS r) |
| 117 | + |
| 118 | +st2ba :: ShortText -> ByteArray |
| 119 | +{-# inline st2ba #-} |
| 120 | +st2ba t = case TS.toShortByteString t of |
| 121 | + SBS x -> ByteArray x |
0 commit comments