aeson-yaml (empty) → 1.0.0.0
raw patch · 8 files changed
+415/−0 lines, 8 filesdep +aesondep +aeson-yamldep +base
Dependencies added: aeson, aeson-yaml, base, bytestring, string-qq, tasty, tasty-discover, tasty-hunit, text, unordered-containers, vector, yaml
Files
- CHANGELOG.md +3/−0
- LICENSE +28/−0
- README.md +32/−0
- aeson-yaml.cabal +82/−0
- bin/JsonToYaml.hs +12/−0
- src/Data/Aeson/Yaml.hs +91/−0
- test/Driver.hs +1/−0
- test/Test/Data/Aeson/Yaml.hs +166/−0
+ CHANGELOG.md view
@@ -0,0 +1,3 @@+# 1.0.0.0++Initial version
+ LICENSE view
@@ -0,0 +1,28 @@+Copyright 2019 Clovyr LLC++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++1. Redistributions of source code must retain the above copyright+notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright+notice, this list of conditions and the following disclaimer in the+documentation and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its+contributors may be used to endorse or promote products derived from+this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,32 @@+# aeson-yaml++BSD3-licensed library to encode any Aeson value as YAML, without a+dependency on an external YAML library like `yaml` (libyaml C FFI) or+`HsYaml` (GPL).++## Usage++```haskell+import qualified Data.Aeson.Yaml as Aeson.Yaml++Aeson.Yaml.encode :: ToJSON a => a -> LazyByteString++-- To encode multiple values, separated by '---' (YAML documents),+-- use `encodeDocuments`.+Aeson.Yaml.encodeDocuments :: ToJSON a => [a] -> LazyByteString++-- To encode values of different types, use `toJSON` from `Data.Aeson`+-- like so:+encodeDocuments [toJSON x, toJSON y, toJSON z]+```++See [bin/JsonToYaml.hs](bin/JsonToYaml.hs) for a simple command-line application+using this library.++## Documentation++[Hackage](https://hackage.haskell.org/package/aeson-yaml)++## License++[BSD3](LICENSE)
+ aeson-yaml.cabal view
@@ -0,0 +1,82 @@+cabal-version: 1.12++name: aeson-yaml+version: 1.0.0.0+homepage: https://github.com/clovyr/aeson-yaml+bug-reports: https://github.com/clovyr/aeson-yaml/issues+author: Patrick Nielsen+maintainer: patrick@clovyr.io+copyright: 2019 Clovyr LLC+license: BSD3+license-file: LICENSE+build-type: Simple+category: Text, Web, JSON, YAML+synopsis: Output any Aeson value as YAML (pure Haskell library)+description:+ This library exposes functions for encoding any Aeson value as YAML. There+ is also support for encoding multiple values into YAML "documents".+ .+ This library is pure Haskell, and does not depend on C FFI with libyaml. It+ is also licensed under the BSD3 license.++extra-source-files:+ README.md+ CHANGELOG.md++source-repository head+ type: git+ location: https://github.com/clovyr/aeson-yaml++flag build-binaries+ description: Build the binaries+ manual: True+ default: False++library+ hs-source-dirs: src+ exposed-modules:+ Data.Aeson.Yaml+ build-depends:+ aeson >= 0.4.0.0 && < 1.5+ , base >= 4.5.0.0 && < 4.13+ , bytestring >= 0.10.4.0 && < 0.11+ , text >= 0.1 && < 1.3+ , unordered-containers >= 0.1.0.0 && < 0.3+ , vector >= 0.1 && < 0.13+ ghc-options:+ -Wall+ default-language: Haskell2010++test-suite test+ hs-source-dirs: test+ main-is: Driver.hs+ other-modules:+ Test.Data.Aeson.Yaml+ build-depends:+ aeson+ , aeson-yaml+ , base+ , bytestring+ -- , hedgehog+ -- , hedgehog-gen-json+ , string-qq+ , tasty+ , tasty-discover+ , tasty-hunit+ , unordered-containers+ , yaml+ type: exitcode-stdio-1.0+ default-language: Haskell2010++executable json-to-yaml+ if !flag(build-binaries)+ buildable: False+ hs-source-dirs: bin+ main-is: JsonToYaml.hs+ build-depends:+ aeson+ , aeson-yaml+ , base+ , bytestring+ ghc-options: -Wall -threaded+ default-language: Haskell2010
+ bin/JsonToYaml.hs view
@@ -0,0 +1,12 @@+module Main where++import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Yaml as Aeson.Yaml+import qualified Data.ByteString.Lazy as ByteString.Lazy++main :: IO ()+main =+ ByteString.Lazy.interact $ \s ->+ case Aeson.eitherDecode' s of+ Left err -> error ("Failed to decode JSON: " <> err)+ Right v -> Aeson.Yaml.encode (v :: Aeson.Value)
+ src/Data/Aeson/Yaml.hs view
@@ -0,0 +1,91 @@+{-|+This library exposes functions for encoding any Aeson value as YAML.+There is also support for encoding multiple values into YAML+"documents".++This library is pure Haskell, and does not depend on C FFI with+libyaml. It is also licensed under the BSD3 license.++This module is meant to be imported qualified.+-}++{-# LANGUAGE OverloadedStrings #-}++module Data.Aeson.Yaml+ ( encode+ , encodeDocuments+ ) where++import Data.Aeson hiding (encode)+import qualified Data.Aeson+import Data.ByteString (ByteString)+import Data.ByteString.Builder (Builder)+import qualified Data.ByteString.Builder as ByteString.Builder+import qualified Data.ByteString.Lazy as ByteString.Lazy+import qualified Data.ByteString.Short as ByteString.Short+import qualified Data.HashMap.Strict as HashMap+import Data.List (sortOn)+import Data.List (intersperse)+import Data.Monoid ((<>), mconcat, mempty)+import qualified Data.Text.Encoding as Text.Encoding+import qualified Data.Vector as Vector++b :: ByteString -> Builder+b = ByteString.Builder.byteString++bl :: ByteString.Lazy.ByteString -> Builder+bl = ByteString.Builder.lazyByteString++bs :: ByteString.Short.ShortByteString -> Builder+bs = ByteString.Builder.shortByteString++indent :: Int -> Builder+indent 0 = mempty+indent n = bs " " <> (indent $! n - 1)++enc :: ToJSON a => a -> ByteString.Lazy.ByteString+enc = Data.Aeson.encode++-- | Encode a value as YAML (lazy bytestring).+encode :: ToJSON a => a -> ByteString.Lazy.ByteString+encode = ByteString.Builder.toLazyByteString . encodeBuilder False 0 . toJSON++-- | Encode multiple values separated by '---'. To encode values of different+-- types, @import Data.Aeson(ToJSON(toJSON))@ and do+-- @encodeDocuments [toJSON x, toJSON y, toJSON z]@.+encodeDocuments :: ToJSON a => [a] -> ByteString.Lazy.ByteString+encodeDocuments =+ ByteString.Builder.toLazyByteString .+ mconcat . intersperse (bs "\n---\n") . map ((encodeBuilder False 0) . toJSON)++encodeBuilder :: Bool -> Int -> Data.Aeson.Value -> Builder+encodeBuilder newlineBeforeObject level value =+ case value of+ Object hm ->+ mconcat $+ (if newlineBeforeObject+ then (prefix :)+ else id) $+ intersperse prefix $ map (keyValue level) (sortOn fst $ HashMap.toList hm)+ where prefix = bs "\n" <> indent level+ Array vec ->+ mconcat $+ (prefix :) $+ intersperse prefix $+ map (encodeBuilder False (level + 1)) (Vector.toList vec)+ where prefix = bs "\n" <> indent level <> bs "- "+ String s -> bl (enc s)+ Number n -> bl (enc n)+ Bool bool -> bl (enc bool)+ Null -> bs "null"+ where+ keyValue level' (k, v) =+ mconcat+ [ b (Text.Encoding.encodeUtf8 k)+ , ":"+ , case v of+ Object _ -> ""+ Array _ -> ""+ _ -> " "+ , encodeBuilder True (level' + 1) v+ ]
+ test/Driver.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF tasty-discover #-}
+ test/Test/Data/Aeson/Yaml.hs view
@@ -0,0 +1,166 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE QuasiQuotes #-}++module Test.Data.Aeson.Yaml where++import qualified Data.Aeson+import qualified Data.ByteString.Lazy+import Data.Either (fromRight)+import qualified Data.HashMap.Strict as HashMap+import Data.String.QQ (s)+import qualified Data.Yaml++-- import Hedgehog+-- import Hedgehog.Gen+-- import Hedgehog.Gen.JSON (genJSONValue, sensibleRanges)+-- import Hedgehog.Range+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (assertEqual, testCase)++import qualified Data.Aeson.Yaml++data TestCase =+ TestCase+ { tcName :: String+ , tcInput :: Data.ByteString.Lazy.ByteString+ , tcOutput :: Data.ByteString.Lazy.ByteString+ }++testCases :: [TestCase]+testCases =+ [ TestCase+ { tcName = "Nested lists and objects"+ , tcInput =+ [s|+{+ "apiVersion": "apps/v1",+ "kind": "Deployment",+ "metadata": {+ "labels": {+ "app": "foo"+ },+ "name": "{{ .Release.Name }}-deployment"+ },+ "spec": {+ "replicas": 1,+ "selector": {+ "matchLabels": {+ "app": "foo"+ }+ },+ "template": {+ "metadata": {+ "labels": {+ "app": "foo"+ },+ "name": "{{ .Release.Name }}-pod"+ },+ "spec": {+ "containers": [+ {+ "command": [+ "/data/bin/foo",+ "--port=7654"+ ],+ "image": "ubuntu:latest",+ "name": "{{ .Release.Name }}-container",+ "ports": [+ {+ "containerPort": 7654+ }+ ],+ "volumeMounts": [+ {+ "mountPath": "/data/mount1",+ "name": "{{ .Release.Name }}-volume-mount1"+ },+ {+ "mountPath": "/data/mount2",+ "name": "{{ .Release.Name }}-volume-mount2"+ }+ ]+ }+ ]+ }+ }+ }+}+|]+ , tcOutput =+ [s|apiVersion: "apps/v1"+kind: "Deployment"+metadata:+ labels:+ app: "foo"+ name: "{{ .Release.Name }}-deployment"+spec:+ replicas: 1+ selector:+ matchLabels:+ app: "foo"+ template:+ metadata:+ labels:+ app: "foo"+ name: "{{ .Release.Name }}-pod"+ spec:+ containers:+ - command:+ - "/data/bin/foo"+ - "--port=7654"+ image: "ubuntu:latest"+ name: "{{ .Release.Name }}-container"+ ports:+ - containerPort: 7654+ volumeMounts:+ - mountPath: "/data/mount1"+ name: "{{ .Release.Name }}-volume-mount1"+ - mountPath: "/data/mount2"+ name: "{{ .Release.Name }}-volume-mount2"|]+ }+ ]++foo :: Data.Aeson.Value+foo = Data.Aeson.Object $ HashMap.fromList [("foo", "bar")]++test_testCases :: TestTree+test_testCases = testGroup "Test Cases" $ map mkTestCase testCases+ where+ mkTestCase TestCase {..} =+ testCase tcName $ do+ assertEqual "Expected output" tcOutput output+ assertEqual+ "libyaml decodes the original value"+ decodedInput+ decodedYaml+ assertEqual+ "Expected documents output"+ ("foo: \"bar\"" <> "\n---\n" <> output)+ (Data.Aeson.Yaml.encodeDocuments [foo, decodedInput])+ where+ output = Data.Aeson.Yaml.encode decodedInput+ decodedInput :: Data.Aeson.Value+ decodedInput =+ fromRight (error "couldn't decode JSON") $+ Data.Aeson.eitherDecode' tcInput+ decodedYaml :: Data.Aeson.Value+ decodedYaml =+ fromRight (error "couldn't decode YAML") $+ Data.Yaml.decodeEither' (Data.ByteString.Lazy.toStrict output)+-- TODO: SBV dep of hedgehog-gen-json doesn't currently build+-- hprop_decodesSameAsLibyaml :: Property+-- hprop_decodesSameAsLibyaml =+-- property $ do+-- v <- forAll $ genJSONValue sensibleRanges+-- let enc1 = Data.Aeson.Yaml.encode v+-- enc2 = Data.Yaml.encode v+-- dec1 =+-- fromRight+-- (error "couldn't decode Aeson.Yaml-encoded value")+-- (Data.Yaml.decodeEither' enc1)+-- dec2 =+-- fromRight+-- (error "couldn't decode Data.Yaml-encoded value")+-- (Data.Yaml.decodeEither' enc2)+-- dec1 === dec2