on master help sign in
Merge pull request #183 from robbiemcmichael/complex-mapping

Complex keys in mappings

commit6539c904a7f0964bd28716c198c0d9ad193e38e4 authorMichael Snoyman <michael@snoyman.com> dateWed 6 Nov 2019, 08:13 (7 years ago) parent40a68afb8c916e
4 files changed+147−40
yaml/ChangeLog.md+5−1 yaml/src/Data/Yaml/Builder.hs+70−24 yaml/src/Data/Yaml/Internal.hs+7−3 yaml/test/Data/YamlSpec.hs+65−12
yaml/ChangeLog.md+5−1
@@ -5,7 +5,11 @@
55* Reduces some of the code duplication between the `encode` and `encodePretty` functions
66* The output of `encodePretty` has been improved:
77 - Multiline strings now use `Literal` style instead of `SingleQuoted`
8− - Special keys are now quoted in maps [#179](https://github.com/snoyberg/yaml/issues/179)
8+ - Special keys are now quoted in mappings [#179](https://github.com/snoyberg/yaml/issues/179)
9+* Support for complex keys in mappings: [#182](https://github.com/snoyberg/yaml/issues/182)
10+ - Adds `complexMapping` function to `Data.Yaml.Builder`
11+ - Decode functions now return a `NonStringKey` error when attempting to decode a mapping with a complex key as it is not possible to decode these to an Aeson `Value`
12+* Adds missing `ToYaml` instances
913
1014## 0.11.1.2
1115
yaml/src/Data/Yaml/Builder.hs+70−24
@@ -9,6 +9,9 @@ module Data.Yaml.Builder
99 , mapping
1010 , namedMapping
1111 , maybeNamedMapping
12+ , mappingComplex
13+ , namedMappingComplex
14+ , maybeNamedMappingComplex
1215 , array
1316 , namedArray
1417 , maybeNamedArray
@@ -37,7 +40,6 @@ module Data.Yaml.Builder
3740
3841import Prelude hiding (null)
3942
40−import Control.Arrow (second)
4143#if MIN_VERSION_aeson(1,0,0)
4244import Data.Aeson.Text (encodeToTextBuilder)
4345#else
@@ -49,6 +51,7 @@ import qualified Data.ByteString.Char8 as S8
4951import Data.Conduit
5052import Data.Scientific (Scientific)
5153import Data.Text (Text, unpack)
54+import qualified Data.Text as T
5255import qualified Data.Text.Encoding as TE
5356import qualified Data.Text.Lazy as TL
5457import Data.Text.Lazy.Builder (toLazyText)
@@ -66,107 +69,148 @@ class ToYaml a where
6669 toYaml :: a -> YamlBuilder
6770instance ToYaml YamlBuilder where
6871 toYaml = id
69−instance ToYaml a => ToYaml [(Text, a)] where
70− toYaml = mapping . map (second toYaml)
72+instance (ToYaml a, ToYaml b) => ToYaml [(a, b)] where
73+ toYaml = mappingComplex . map (\(k, v) -> (toYaml k, toYaml v))
7174instance ToYaml a => ToYaml [a] where
7275 toYaml = array . map toYaml
7376instance ToYaml Text where
7477 toYaml = string
78+instance ToYaml String where
79+ toYaml = string . T.pack
7580instance ToYaml Int where
76− toYaml i = YamlBuilder (EventScalar (S8.pack $ show i) IntTag PlainNoTag Nothing:)
81+ toYaml i = YamlBuilder (EventScalar (S8.pack $ show i) NoTag PlainNoTag Nothing:)
82+instance ToYaml Double where
83+ toYaml i = YamlBuilder (EventScalar (S8.pack $ show i) NoTag PlainNoTag Nothing:)
84+instance ToYaml Scientific where
85+ toYaml = scientific
86+instance ToYaml Bool where
87+ toYaml = bool
88+instance ToYaml a => ToYaml (Maybe a) where
89+ toYaml = maybe null toYaml
7790
7891-- |
79−-- @since 0.11.0
92+-- @since 0.10.3.0
8093maybeNamedMapping :: Maybe Text -> [(Text, YamlBuilder)] -> YamlBuilder
81−maybeNamedMapping anchor pairs = YamlBuilder $ \rest ->
82− EventMappingStart NoTag AnyMapping (unpack <$> anchor) : foldr addPair (EventMappingEnd : rest) pairs
94+maybeNamedMapping anchor pairs = maybeNamedMappingComplex anchor complexPairs
8395 where
84− addPair (key, YamlBuilder value) after = unYamlBuilder (string key) $ value after
96+ complexPairs = map (\(k, v) -> (string k, v)) pairs
8597
98+-- |
99+-- @since 0.8.7
86100mapping :: [(Text, YamlBuilder)] -> YamlBuilder
87101mapping = maybeNamedMapping Nothing
88102
89103-- |
90−-- @since 0.11.0
104+-- @since 0.10.3.0
91105namedMapping :: Text -> [(Text, YamlBuilder)] -> YamlBuilder
92106namedMapping name = maybeNamedMapping $ Just name
93107
94108-- |
95−-- @since 0.11.0
109+-- @since 0.11.2.0
110+maybeNamedMappingComplex :: Maybe Text -> [(YamlBuilder, YamlBuilder)] -> YamlBuilder
111+maybeNamedMappingComplex anchor pairs = YamlBuilder $ \rest ->
112+ EventMappingStart NoTag AnyMapping (unpack <$> anchor) : foldr addPair (EventMappingEnd : rest) pairs
113+ where
114+ addPair (YamlBuilder key, YamlBuilder value) after = key $ value after
115+
116+-- |
117+-- @since 0.11.2.0
118+mappingComplex :: [(YamlBuilder, YamlBuilder)] -> YamlBuilder
119+mappingComplex = maybeNamedMappingComplex Nothing
120+
121+-- |
122+-- @since 0.11.2.0
123+namedMappingComplex :: Text -> [(YamlBuilder, YamlBuilder)] -> YamlBuilder
124+namedMappingComplex name = maybeNamedMappingComplex $ Just name
125+
126+-- |
127+-- @since 0.10.3.0
96128maybeNamedArray :: Maybe Text -> [YamlBuilder] -> YamlBuilder
97129maybeNamedArray anchor bs =
98130 YamlBuilder $ (EventSequenceStart NoTag AnySequence (unpack <$> anchor):) . flip (foldr go) bs . (EventSequenceEnd:)
99131 where
100132 go (YamlBuilder b) = b
101133
134+-- |
135+-- @since 0.8.7
102136array :: [YamlBuilder] -> YamlBuilder
103137array = maybeNamedArray Nothing
104138
105139-- |
106−-- @since 0.11.0
140+-- @since 0.10.3.0
107141namedArray :: Text -> [YamlBuilder] -> YamlBuilder
108142namedArray name = maybeNamedArray $ Just name
109143
110144-- |
111−-- @since 0.11.0
145+-- @since 0.10.3.0
112146maybeNamedString :: Maybe Text -> Text -> YamlBuilder
113147maybeNamedString anchor s = YamlBuilder (stringScalar defaultStringStyle anchor s :)
114148
149+-- |
150+-- @since 0.8.7
115151string :: Text -> YamlBuilder
116152string = maybeNamedString Nothing
117153
118154-- |
119−-- @since 0.11.0
155+-- @since 0.10.3.0
120156namedString :: Text -> Text -> YamlBuilder
121157namedString name = maybeNamedString $ Just name
122158
123159-- Use aeson's implementation which gets rid of annoying decimal points
124160-- |
125−-- @since 0.11.0
161+-- @since 0.10.3.0
126162maybeNamedScientific :: Maybe Text -> Scientific -> YamlBuilder
127−maybeNamedScientific anchor n = YamlBuilder (EventScalar (TE.encodeUtf8 $ TL.toStrict $ toLazyText $ encodeToTextBuilder (Number n)) IntTag PlainNoTag (unpack <$> anchor) :)
163+maybeNamedScientific anchor n = YamlBuilder (EventScalar (TE.encodeUtf8 $ TL.toStrict $ toLazyText $ encodeToTextBuilder (Number n)) NoTag PlainNoTag (unpack <$> anchor) :)
128164
165+-- |
166+-- @since 0.8.13
129167scientific :: Scientific -> YamlBuilder
130168scientific = maybeNamedScientific Nothing
131169
132170-- |
133−-- @since 0.11.0
171+-- @since 0.10.3.0
134172namedScientific :: Text -> Scientific -> YamlBuilder
135173namedScientific name = maybeNamedScientific $ Just name
136174
175+-- |
176+-- @since 0.8.13
137177{-# DEPRECATED number "Use scientific" #-}
138178number :: Scientific -> YamlBuilder
139179number = scientific
140180
141181-- |
142−-- @since 0.11.0
182+-- @since 0.10.3.0
143183maybeNamedBool :: Maybe Text -> Bool -> YamlBuilder
144−maybeNamedBool anchor True = YamlBuilder (EventScalar "true" BoolTag PlainNoTag (unpack <$> anchor) :)
145−maybeNamedBool anchor False = YamlBuilder (EventScalar "false" BoolTag PlainNoTag (unpack <$> anchor) :)
184+maybeNamedBool anchor True = YamlBuilder (EventScalar "true" NoTag PlainNoTag (unpack <$> anchor) :)
185+maybeNamedBool anchor False = YamlBuilder (EventScalar "false" NoTag PlainNoTag (unpack <$> anchor) :)
146186
187+-- |
188+-- @since 0.8.13
147189bool :: Bool -> YamlBuilder
148190bool = maybeNamedBool Nothing
149191
150192-- |
151−-- @since 0.11.0
193+-- @since 0.10.3.0
152194namedBool :: Text -> Bool -> YamlBuilder
153195namedBool name = maybeNamedBool $ Just name
154196
155197-- |
156−-- @since 0.11.0
198+-- @since 0.10.3.0
157199maybeNamedNull :: Maybe Text -> YamlBuilder
158−maybeNamedNull anchor = YamlBuilder (EventScalar "null" NullTag PlainNoTag (unpack <$> anchor) :)
200+maybeNamedNull anchor = YamlBuilder (EventScalar "null" NoTag PlainNoTag (unpack <$> anchor) :)
159201
202+-- |
203+-- @since 0.8.13
160204null :: YamlBuilder
161205null = maybeNamedNull Nothing
162206
163207-- |
164−-- @since 0.11.0
208+-- @since 0.10.3.0
165209namedNull :: Text -> YamlBuilder
166210namedNull name = maybeNamedNull $ Just name
167211
168212-- |
169−-- @since 0.11.0
213+-- @since 0.10.3.0
170214alias :: Text -> YamlBuilder
171215alias anchor = YamlBuilder (EventAlias (unpack anchor) :)
172216
@@ -177,6 +221,8 @@ toEvents (YamlBuilder front) =
177221toSource :: (Monad m, ToYaml a) => a -> ConduitM i Event m ()
178222toSource = mapM_ yield . toEvents . toYaml
179223
224+-- |
225+-- @since 0.8.7
180226toByteString :: ToYaml a => a -> ByteString
181227toByteString = toByteStringWith defaultFormatOptions
182228
yaml/src/Data/Yaml/Internal.hs+7−3
@@ -31,7 +31,7 @@ import Control.Monad.Trans.Resource (ResourceT, runResourceT)
3131import Control.Monad.State.Strict
3232import Control.Monad.Reader
3333import Data.Aeson
34−import Data.Aeson.Internal (JSONPath, JSONPathElement(..))
34+import Data.Aeson.Internal (JSONPath, JSONPathElement(..), formatError)
3535import Data.Aeson.Types hiding (parse)
3636import qualified Data.Attoparsec.Text as Atto
3737import Data.Bits (shiftL, (.|.))
@@ -68,6 +68,7 @@ data ParseException = NonScalarKey
6868 | InvalidYaml (Maybe YamlException)
6969 | AesonException String
7070 | OtherParseException SomeException
71+ | NonStringKey JSONPath
7172 | NonStringKeyAlias Y.AnchorName Value
7273 | CyclicIncludes
7374 | LoadSettingsException FilePath ParseException
@@ -116,6 +117,7 @@ prettyPrintParseException pe = case pe of
116117 ]
117118 AesonException s -> "Aeson exception:\n" ++ s
118119 OtherParseException exc -> "Generic parse exception:\n" ++ show exc
120+ NonStringKey path -> formatError path "Non-string keys are not supported"
119121 NonStringKeyAlias anchor value -> unlines
120122 [ "Non-string key alias:"
121123 , " Anchor name: " ++ anchor
@@ -258,7 +260,9 @@ parseM mergedKeys a front = do
258260 Nothing -> liftIO $ throwIO $ UnknownAlias an
259261 Just (String t) -> return t
260262 Just v -> liftIO $ throwIO $ NonStringKeyAlias an v
261− _ -> liftIO $ throwIO $ UnexpectedEvent me Nothing
263+ _ -> do
264+ path <- ask
265+ liftIO $ throwIO $ NonStringKey path
262266
263267 (mergedKeys', al') <- local (Key s :) $ do
264268 o <- parseO
@@ -329,7 +333,7 @@ defaultStringStyle = \s ->
329333 ()
330334 | "\n" `T.isInfixOf` s -> ( NoTag, Literal )
331335 | isSpecialString s -> ( NoTag, SingleQuoted )
332− | otherwise -> ( StrTag, PlainNoTag )
336+ | otherwise -> ( NoTag, PlainNoTag )
333337
334338-- | Determine whether a string must be quoted in YAML and can't appear as plain text.
335339-- Useful if you want to use 'setStringStyle'.
yaml/test/Data/YamlSpec.hs+65−12
@@ -27,6 +27,7 @@ import System.Directory (createDirectory, createDirectoryIfMissing)
2727import Test.Mockery.Directory
2828
2929import qualified Data.Yaml as D
30+import qualified Data.Yaml.Builder as B
3031import qualified Data.Yaml.Internal as Internal
3132import qualified Data.Yaml.Pretty as Pretty
3233import Data.Yaml (object, array, (.=))
@@ -65,15 +66,28 @@ testJSON = TestJSON
6566
6667shouldDecode :: (Show a, D.FromJSON a, Eq a) => B8.ByteString -> a -> IO ()
6768shouldDecode bs expected = do
68− actual <- D.decodeThrow bs
69− actual `shouldBe` expected
69+ actual <- D.decodeThrow bs
70+ actual `shouldBe` expected
71+
72+shouldDecodeEvents :: B8.ByteString -> [Y.Event] -> IO ()
73+shouldDecodeEvents bs expected = do
74+ actual <- runConduitRes $ Y.decode bs .| CL.consume
75+ map anyStyle actual `shouldBe` map anyStyle expected
76+
77+anyStyle :: Y.Event -> Y.Event
78+anyStyle (Y.EventScalar bs tag _ anchor) = Y.EventScalar bs tag Y.Any anchor
79+anyStyle (Y.EventSequenceStart tag _ anchor) = Y.EventSequenceStart tag Y.AnySequence anchor
80+anyStyle (Y.EventMappingStart tag _ anchor) = Y.EventMappingStart tag Y.AnyMapping anchor
81+anyStyle event = event
7082
7183testEncodeWith :: Y.FormatOptions -> [Y.Event] -> IO BS.ByteString
72−testEncodeWith opts es = runConduitRes (CL.sourceList events .| Y.encodeWith opts)
73− where
74− events =
75− [Y.EventStreamStart, Y.EventDocumentStart] ++ es ++
76− [Y.EventDocumentEnd, Y.EventStreamEnd]
84+testEncodeWith opts es = runConduitRes $ CL.sourceList (eventStream es) .| Y.encodeWith opts
85+
86+eventStream :: [Y.Event] -> [Y.Event]
87+eventStream events =
88+ [Y.EventStreamStart, Y.EventDocumentStart]
89+ ++ events
90+ ++ [Y.EventDocumentEnd, Y.EventStreamEnd]
7791
7892main :: IO ()
7993main = hspec spec
@@ -112,6 +126,9 @@ spec = do
112126 it "encode/decode" caseEncodeDecodeDataPretty
113127 it "encode/decode strings" caseEncodeDecodeStringsPretty
114128 it "processes datatypes" caseDataTypesPretty
129+ describe "Data.Yaml.Builder" $ do
130+ it "encode/decode" caseEncodeDecodeDataBuilder
131+ it "encode/decode complex mapping" caseEncodeDecodeComplexMappingBuilder
115132 describe "Data.Yaml aliases" $ do
116133 it "simple scalar alias" caseSimpleScalarAlias
117134 it "simple sequence alias" caseSimpleSequenceAlias
@@ -478,13 +495,9 @@ caseEncodeDecode = do
478495caseEncodeDecodeEvents :: Assertion
479496caseEncodeDecodeEvents = do
480497 let events = Internal.objToEvents D.defaultStringStyle testJSON []
481− result <- Internal.decodeHelper_ . CL.sourceList $ toStream events
498+ result <- Internal.decodeHelper_ . CL.sourceList $ eventStream events
482499 let (_, value) = either (error . show) id result
483500 value @?= testJSON
484− where
485− toStream es =
486− [Y.EventStreamStart, Y.EventDocumentStart] ++ es ++
487− [Y.EventDocumentEnd, Y.EventStreamEnd]
488501
489502caseEncodeDecodeFile :: Assertion
490503caseEncodeDecodeFile = withFile "" $ \tmpPath -> do
@@ -535,6 +548,24 @@ sample = array
535548 , ("bar3", D.String "")
536549 ]
537550 , D.String ""
551+ , D.Number 1
552+ , D.Number 0.1
553+ , D.Bool True
554+ , D.Null
555+ ]
556+
557+sampleBuilder :: B.YamlBuilder
558+sampleBuilder = B.array
559+ [ B.string "foo"
560+ , B.mapping
561+ [ ("bar1", B.string "bar2")
562+ , ("bar3", B.string "")
563+ ]
564+ , B.string ""
565+ , B.scientific 1
566+ , B.scientific 0.1
567+ , B.bool True
568+ , B.null
538569 ]
539570
540571caseEncodeDecodeData :: Assertion
@@ -544,6 +575,28 @@ caseEncodeDecodeDataPretty :: Assertion
544575caseEncodeDecodeDataPretty =
545576 Pretty.encodePretty Pretty.defConfig sample `shouldDecode` sample
546577
578+caseEncodeDecodeDataBuilder :: Assertion
579+caseEncodeDecodeDataBuilder = do
580+ let events = B.unYamlBuilder sampleBuilder []
581+ bs <- testEncodeWith Y.defaultFormatOptions events
582+ bs `shouldDecodeEvents` eventStream events
583+
584+caseEncodeDecodeComplexMappingBuilder :: Assertion
585+caseEncodeDecodeComplexMappingBuilder = do
586+ let events = B.unYamlBuilder builder []
587+ bs <- testEncodeWith Y.defaultFormatOptions events
588+ bs `shouldDecodeEvents` eventStream events
589+ where
590+ builder :: B.YamlBuilder
591+ builder = B.mappingComplex
592+ [ ( B.mapping
593+ [ ("foo", B.scientific 1)
594+ , ("bar", B.scientific 2)
595+ ]
596+ , B.bool True
597+ )
598+ ]
599+
547600caseEncodeDecodeFileData :: Assertion
548601caseEncodeDecodeFileData = withFile "" $ \fp -> do
549602 D.encodeFile fp sample