Skip to content

Commit c4ecc71

Browse files
committed
Add Json.Flatten
1 parent bf03e0d commit c4ecc71

File tree

5 files changed

+191
-3
lines changed

5 files changed

+191
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Revision history for json-syntax
22

3+
## 0.2.3.0 -- 2022-??-??
4+
5+
* Add `Json.Flatten` module.
6+
37
## 0.2.2.0 -- 2022-07-15
48

59
* Build with GHC 9.2.3.

common/Person.hs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
{-# LANGUAGE QuasiQuotes #-}
3+
module Person
4+
( encodedPerson
5+
, encodedFlattenedPerson
6+
) where
7+
8+
import Data.ByteString (ByteString)
9+
import Data.ByteString.Short (ShortByteString,toShort)
10+
import Data.Primitive (ByteArray)
11+
import Data.Text.Encoding (encodeUtf8)
12+
import NeatInterpolation (text)
13+
14+
import qualified Data.Primitive as PM
15+
import qualified Data.ByteString.Short.Internal as BSS
16+
17+
shortByteStringToByteArray :: ShortByteString -> ByteArray
18+
shortByteStringToByteArray (BSS.SBS x) = PM.ByteArray x
19+
20+
encodedPerson :: ByteArray
21+
encodedPerson =
22+
shortByteStringToByteArray (toShort byteStringPerson)
23+
24+
encodedFlattenedPerson :: ByteArray
25+
encodedFlattenedPerson =
26+
shortByteStringToByteArray (toShort byteStringFlattenedPerson)
27+
28+
byteStringPerson :: ByteString
29+
byteStringPerson = encodeUtf8
30+
[text|
31+
{ "name": "bilbo"
32+
, "occupation":
33+
{ "name": "burglar"
34+
, "start": "2022-05-30"
35+
}
36+
, "height": 124
37+
, "favorites": ["adventures","lunch"]
38+
}
39+
|]
40+
41+
byteStringFlattenedPerson :: ByteString
42+
byteStringFlattenedPerson = encodeUtf8
43+
[text|
44+
{ "name": "bilbo"
45+
, "occupation.name": "burglar"
46+
, "occupation.start": "2022-05-30"
47+
, "height": 124
48+
, "favorites": ["adventures","lunch"]
49+
}
50+
|]
51+

json-syntax.cabal

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cabal-version: 2.2
22
name: json-syntax
3-
version: 0.2.2.0
3+
version: 0.2.3.0
44
synopsis: High-performance JSON parser and encoder
55
description:
66
This library parses JSON into a @Value@ type that is consistent with the
@@ -28,13 +28,14 @@ extra-source-files: CHANGELOG.md
2828
library
2929
exposed-modules:
3030
Json
31+
Json.Flatten
3132
Json.Smile
3233
build-depends:
3334
, array-builder >=0.1 && <0.2
3435
, array-chunks >=0.1.3 && <0.2
3536
, base >=4.12 && <5
3637
, bytebuild >=0.3.10 && <0.4
37-
, byteslice >=0.1.3 && <0.3
38+
, byteslice >=0.2.9 && <0.3
3839
, bytesmith >=0.3.8 && <0.4
3940
, bytestring >=0.10.8 && <0.12
4041
, integer-gmp >=1.0 && <1.2
@@ -57,6 +58,7 @@ test-suite test
5758
main-is: Main.hs
5859
other-modules:
5960
Twitter100
61+
Person
6062
ghc-options: -Wall -O2
6163
build-depends:
6264
, QuickCheck >=2.14.2

src/Json/Flatten.hs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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

test/Main.hs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
{-# language LambdaCase #-}
2+
{-# language MultiWayIf #-}
23
{-# language OverloadedStrings #-}
34
{-# language ScopedTypeVariables #-}
45

56
import Control.Monad (when)
6-
import Data.Bytes (Bytes)
77
import Data.ByteString.Short.Internal (ShortByteString(SBS))
8+
import Data.Bytes (Bytes)
89
import Data.Primitive (ByteArray(ByteArray))
910
import Data.Scientific (Scientific,scientific)
1011
import Data.Text.Short (ShortText)
12+
import Json.Flatten (flatten)
13+
import Person (encodedPerson,encodedFlattenedPerson)
1114
import System.IO (withFile,IOMode(..))
1215
import Test.QuickCheck ((===))
1316
import Test.Tasty (defaultMain,testGroup,TestTree)
@@ -217,6 +220,13 @@ tests = testGroup "Tests"
217220
Right j -> case J.decode (BChunks.concat (Builder.run 1 (J.encode j))) of
218221
Left _ -> fail "encode did not produce a document that could be decoded"
219222
Right j' -> when (j /= j') (fail "document was not the same after roundtrip")
223+
, testGroup "flatten"
224+
[ THU.testCase "Person" $
225+
if | Right original <- J.decode (Bytes.fromByteArray encodedPerson)
226+
, Right flattened <- J.decode (Bytes.fromByteArray encodedFlattenedPerson)
227+
-> flattened @=? flatten '.' original
228+
| otherwise -> fail "bad input from common/Person.hs"
229+
]
220230
]
221231

222232
jsonFromPrintableStrings :: [QC.PrintableString] -> J.Value

0 commit comments

Comments
 (0)