packages feed

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 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