packages feed

aeson-yaml 1.0.0.0 → 1.0.1.0

raw patch · 5 files changed

+193/−41 lines, 5 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Data.Aeson.Yaml: encodeQuoted :: ToJSON a => a -> ByteString
+ Data.Aeson.Yaml: encodeQuotedDocuments :: ToJSON a => [a] -> ByteString

Files

CHANGELOG.md view
@@ -1,3 +1,20 @@-# 1.0.0.0+# Changelog++## 1.0.1.0 - 2019-10-15++### Added++  - `encodeQuoted`: Encodes with all keys/strings quoted+  - `encodeQuotedDocuments`: Encodes documents with all keys/strings quoted++### Changed++  - Simple strings (scalars) are now written unquoted.++  - Multi-line strings (with trailing newlines) are now written as literal+    block scalars.+++## 1.0.0.0 - 2019-09-30  Initial version
README.md view
@@ -1,8 +1,6 @@ # 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).+BSD3-licensed, pure Haskell library to encode any Aeson value as YAML.  ## Usage @@ -30,3 +28,11 @@ ## License  [BSD3](LICENSE)++## Motivation++This library does not depend on any external YAML library with C bindings,+like `yaml`, or a restrictive license, like `HsYaml` (GPLv3). Note, though,+that this library can only be used for encoding, not decoding.++This library also works with GHCJS and Eta.
aeson-yaml.cabal view
@@ -1,7 +1,7 @@ cabal-version: 1.12  name:           aeson-yaml-version:        1.0.0.0+version:        1.0.1.0 homepage:       https://github.com/clovyr/aeson-yaml bug-reports:    https://github.com/clovyr/aeson-yaml/issues author:         Patrick Nielsen@@ -38,7 +38,7 @@       Data.Aeson.Yaml   build-depends:       aeson >= 0.4.0.0 && < 1.5-    , base >= 4.5.0.0 && < 4.13+    , base >= 4.8.2.0 && < 5     , bytestring >= 0.10.4.0 && < 0.11     , text >= 0.1 && < 1.3     , unordered-containers >= 0.1.0.0 && < 0.3
src/Data/Aeson/Yaml.hs view
@@ -8,12 +8,13 @@  This module is meant to be imported qualified. -}- {-# LANGUAGE OverloadedStrings #-}  module Data.Aeson.Yaml   ( encode   , encodeDocuments+  , encodeQuoted+  , encodeQuotedDocuments   ) where  import Data.Aeson hiding (encode)@@ -23,10 +24,12 @@ 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 Data.Char (isDigit) import qualified Data.HashMap.Strict as HashMap-import Data.List (sortOn)-import Data.List (intersperse)+import Data.List (intersperse, sortOn) import Data.Monoid ((<>), mconcat, mempty)+import qualified Data.Text as Text+import Data.Text (Text) import qualified Data.Text.Encoding as Text.Encoding import qualified Data.Vector as Vector @@ -43,23 +46,41 @@ 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 v =+  ByteString.Builder.toLazyByteString $+  encodeBuilder False False 0 (toJSON v) <> bs "\n" --- | Encode multiple values separated by '---'. To encode values of different+-- | Encode multiple values separated by '\n---\n'. 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)+encodeDocuments vs = ByteString.Builder.toLazyByteString $ output <> bs "\n"+  where+    output =+      mconcat $+      intersperse (bs "\n---\n") $ map (encodeBuilder False False 0 . toJSON) vs -encodeBuilder :: Bool -> Int -> Data.Aeson.Value -> Builder-encodeBuilder newlineBeforeObject level value =+-- | Encode a value as YAML (lazy bytestring). Keys and strings are always+-- quoted.+encodeQuoted :: ToJSON a => a -> ByteString.Lazy.ByteString+encodeQuoted v =+  ByteString.Builder.toLazyByteString $+  encodeBuilder True False 0 (toJSON v) <> bs "\n"++-- | Encode multiple values separated by '\n---\n'. Keys and strings are always+-- quoted.+encodeQuotedDocuments :: ToJSON a => [a] -> ByteString.Lazy.ByteString+encodeQuotedDocuments vs =+  ByteString.Builder.toLazyByteString $ output <> bs "\n"+  where+    output =+      mconcat $+      intersperse (bs "\n---\n") $ map (encodeBuilder True False 0 . toJSON) vs++encodeBuilder :: Bool -> Bool -> Int -> Data.Aeson.Value -> Builder+encodeBuilder alwaysQuote newlineBeforeObject level value =   case value of     Object hm ->       mconcat $@@ -72,20 +93,75 @@       mconcat $       (prefix :) $       intersperse prefix $-      map (encodeBuilder False (level + 1)) (Vector.toList vec)+      map (encodeBuilder alwaysQuote 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)+    String s -> encodeText True alwaysQuote level s+    Number n -> bl (Data.Aeson.encode n)+    Bool bool -> bl (Data.Aeson.encode bool)     Null -> bs "null"   where     keyValue level' (k, v) =       mconcat-        [ b (Text.Encoding.encodeUtf8 k)+        [ encodeText False alwaysQuote level k         , ":"         , case v of             Object _ -> ""             Array _ -> ""             _ -> " "-        , encodeBuilder True (level' + 1) v+        , encodeBuilder alwaysQuote True (level' + 1) v         ]++encodeText :: Bool -> Bool -> Int -> Text -> Builder+encodeText canMultiline alwaysQuote level s+  | canMultiline && "\n" `Text.isSuffixOf` s = encodeLines level (Text.lines s)+  | alwaysQuote && unquotable =+    bs "'" <> b (Text.Encoding.encodeUtf8 s) <> bs "'"+  | alwaysQuote || not unquotable = bl $ Data.Aeson.encode s+  | otherwise = b (Text.Encoding.encodeUtf8 s)+  where+    unquotable =+      s /= "" &&+      (not $ isSpecial s) &&+      isSafeAscii (Text.head s) &&+      not (Text.all isNumberRelated s) && Text.all isAllowed s+    isSpecial s'+      | Text.length s > 5 = False+      | otherwise =+        case Text.toLower s' of+          "true" -> True+          "false" -> True+          "on" -> True+          "off" -> True+          "y" -> True+          "yes" -> True+          "n" -> True+          "no" -> True+          _ -> False+    isSafeAscii c =+      (c >= 'a' && c <= 'z') ||+      (c >= 'A' && c <= 'Z') ||+      (c >= '0' && c <= '9') || c == '/' || c == '_' || c == '.' || c == '='+    isNumberRelated c = isDigit c || c == '.' || c == 'e'+    isAllowed c+      -- We don't include ' ' here to avoid sequences like " -" and ": "+     = isSafeAscii c || c == '-' || c == ':'++encodeLines :: Int -> [Text] -> Builder+encodeLines level ls =+  mconcat $+  (prefix :) $+  intersperse (bs "\n" <> indent level) $ map (b . Text.Encoding.encodeUtf8) ls+  where+    prefix =+      mconcat+        [ bs "|"+        , if needsIndicator+            then bs "2"+            else mempty+        , "\n"+        , indent level+        ]+    needsIndicator =+      case ls of+        (line:_) -> " " `Text.isPrefixOf` line+        _ -> False
test/Test/Data/Aeson/Yaml.hs view
@@ -18,13 +18,14 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertEqual, testCase) -import qualified Data.Aeson.Yaml+import Data.Aeson.Yaml  data TestCase =   TestCase     { tcName :: String     , tcInput :: Data.ByteString.Lazy.ByteString     , tcOutput :: Data.ByteString.Lazy.ByteString+    , tcAlwaysQuote :: Bool     }  testCases :: [TestCase]@@ -34,6 +35,18 @@       , tcInput =           [s| {+   "nullValue": null,+   "isTrue": true,+   "isFalse": false,+   "numberString": "12345",+   "quoted ! key": true,+   "ON": "ON",+   "off": "off",+   "piString": "3.14",+   "expString": "1e3",+   "multiLine": "The first line is followed by the\nsecond line\n",+   "multiLineWithSpaces": "  This has\nextra spaces at the beginning\n",+   "notMultiline": "This won't be\nmulti-lined",    "apiVersion": "apps/v1",    "kind": "Deployment",    "metadata": {@@ -70,6 +83,7 @@                         "containerPort": 7654                      }                   ],+                  "script": "#!/bin/bash\necho hello world\n",                   "volumeMounts": [                      {                         "mountPath": "/data/mount1",@@ -88,37 +102,67 @@ } |]       , tcOutput =-          [s|apiVersion: "apps/v1"-kind: "Deployment"+          [s|"ON": "ON"+apiVersion: apps/v1+expString: "1e3"+isFalse: false+isTrue: true+kind: Deployment metadata:   labels:-    app: "foo"+    app: foo   name: "{{ .Release.Name }}-deployment"+multiLine: |+  The first line is followed by the+  second line+multiLineWithSpaces: |2+    This has+  extra spaces at the beginning+notMultiline: "This won't be\nmulti-lined"+nullValue: null+numberString: "12345"+"off": "off"+piString: "3.14"+"quoted ! key": true spec:   replicas: 1   selector:     matchLabels:-      app: "foo"+      app: foo   template:     metadata:       labels:-        app: "foo"+        app: foo       name: "{{ .Release.Name }}-pod"     spec:       containers:         - command:-            - "/data/bin/foo"+            - /data/bin/foo             - "--port=7654"-          image: "ubuntu:latest"+          image: ubuntu:latest           name: "{{ .Release.Name }}-container"           ports:             - containerPort: 7654+          script: |+            #!/bin/bash+            echo hello world           volumeMounts:-            - mountPath: "/data/mount1"+            - mountPath: /data/mount1               name: "{{ .Release.Name }}-volume-mount1"-            - mountPath: "/data/mount2"-              name: "{{ .Release.Name }}-volume-mount2"|]+            - mountPath: /data/mount2+              name: "{{ .Release.Name }}-volume-mount2"+|]+      , tcAlwaysQuote = False       }+  , TestCase+      { tcName = "Quoted"+      , tcInput = [s|{"foo": "bar", "baz": "quux"}|]+      , tcOutput =+          [s|'baz': 'quux'+'foo': 'bar'+|]+      , tcAlwaysQuote = True+      }   ]  foo :: Data.Aeson.Value@@ -134,12 +178,21 @@           "libyaml decodes the original value"           decodedInput           decodedYaml-        assertEqual-          "Expected documents output"-          ("foo: \"bar\"" <> "\n---\n" <> output)-          (Data.Aeson.Yaml.encodeDocuments [foo, decodedInput])+        if tcAlwaysQuote+          then assertEqual+                 "Expected documents output"+                 ("'foo': 'bar'" <> "\n---\n" <> output)+                 (encodeQuotedDocuments [foo, decodedInput])+          else assertEqual+                 "Expected documents output"+                 ("foo: bar" <> "\n---\n" <> output)+                 (encodeDocuments [foo, decodedInput])       where-        output = Data.Aeson.Yaml.encode decodedInput+        output =+          (if tcAlwaysQuote+             then encodeQuoted+             else encode)+            decodedInput         decodedInput :: Data.Aeson.Value         decodedInput =           fromRight (error "couldn't decode JSON") $