hocon (empty) → 0.1.0.0
raw patch · 15 files changed
+547/−0 lines, 15 filesdep +MissingHdep +basedep +hoconsetup-changed
Dependencies added: MissingH, base, hocon, hspec, parsec, split
Files
- ChangeLog.md +3/−0
- LICENSE +30/−0
- README.md +44/−0
- Setup.hs +2/−0
- app/Main.hs +4/−0
- hocon.cabal +81/−0
- src/Data/Bifunctor/Extra.hs +11/−0
- src/Data/HOCON.hs +99/−0
- src/Data/Map.hs +24/−0
- src/Text/Parser/HOCON.hs +12/−0
- src/Text/Parser/HOCON/Internal.hs +132/−0
- test/Data/MapSpec.hs +11/−0
- test/Spec.hs +1/−0
- test/Text/Parser/HOCON/InternalSpec.hs +74/−0
- test/Text/Parser/HOCONSpec.hs +19/−0
+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for hocon++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author name here (c) 2020++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * 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.++ * Neither the name of Author name here nor the names of other+ 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+OWNER 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,44 @@+# hocon++Small library for [Typesafe's configuration specification](https://github.com/lightbend/config) built with parsec.++```+foo = bar+bar = 123+someObject {+ wow = "this is nice!"+}+```++While there are many configuration notations and formats around, HOCON stands in a comfortable place, albeit a bit underrated. Being a superset to JSON means it inherits its great readability, as well as extending it with some very nice features, such as comments, no need to add `"` for properties and values (unless some special character is required), digging up nested values, among others.++## Sounds nice, how do I use it?++First of all, import the basic stuff:+```haskell+import Data.HOCON+import Text.Parser.HOCON+```++The first module exports the tree-like structure typical of JSON (if you're familiar with `aeson`, then it's really the same structure. It's not using _that_ structure because no way I'm including that just for the structure) and the accessor functions, while the second exports the parser function:+```haskell+parseHOCON :: String -> Either ParseError Config+```++If you've already used Typesafe's config on the JVM, then the functions will make you feel just like at home:+```haskell+getConfig :: String -> Config -> Maybe Config+getNumber :: String -> Config -> Maybe Double+getString :: String -> Config -> Maybe String+getBoolean :: String -> Config -> Maybe Bool+getList :: String -> Config -> Maybe [Config]+hasPath :: String -> Config -> Bool+```++## Missing features / what's to come+* Actually support comments+* Multiline strings+* Include another file and merge them+* Object substitution++Pull requests are always welcome!
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,4 @@+module Main where++main :: IO ()+main = return ()
+ hocon.cabal view
@@ -0,0 +1,81 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.33.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 12270ead08a8273a5e886852616f4732e424ebe6ce7641968b4c92725e95f0c8++name: hocon+version: 0.1.0.0+description: Please see the README on GitHub at <https://github.com/githubuser/hocon#readme>+homepage: https://github.com/githubuser/hocon#readme+bug-reports: https://github.com/githubuser/hocon/issues+author: Author name here+maintainer: example@example.com+copyright: 2020 Author name here+license: BSD3+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ ChangeLog.md++source-repository head+ type: git+ location: https://github.com/githubuser/hocon++library+ exposed-modules:+ Data.Bifunctor.Extra+ Data.HOCON+ Data.Map+ Text.Parser.HOCON+ Text.Parser.HOCON.Internal+ other-modules:+ Paths_hocon+ hs-source-dirs:+ src+ build-depends:+ MissingH <=1.4.3.0+ , base >=4.7 && <5+ , hspec+ , parsec <=3.1.14.0+ , split <=0.2.3.4+ default-language: Haskell2010++executable hocon-exe+ main-is: Main.hs+ other-modules:+ Paths_hocon+ hs-source-dirs:+ app+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ MissingH <=1.4.3.0+ , base >=4.7 && <5+ , hocon+ , hspec+ , parsec <=3.1.14.0+ , split <=0.2.3.4+ default-language: Haskell2010++test-suite hocon-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Data.MapSpec+ Text.Parser.HOCON.InternalSpec+ Text.Parser.HOCONSpec+ Paths_hocon+ hs-source-dirs:+ test+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ MissingH <=1.4.3.0+ , base >=4.7 && <5+ , hocon+ , hspec+ , parsec <=3.1.14.0+ , split <=0.2.3.4+ default-language: Haskell2010
+ src/Data/Bifunctor/Extra.hs view
@@ -0,0 +1,11 @@+module Data.Bifunctor.Extra+ ( module Data.Bifunctor+ , mapValues+ )+where++import Data.Bifunctor (Bifunctor(..))+import Data.Map (Map)++mapValues :: (b -> c) -> Map a b -> Map a c+mapValues f = map (second f)
+ src/Data/HOCON.hs view
@@ -0,0 +1,99 @@+module Data.HOCON+ ( Config(..)+ , getNode+ , hasPath+ , getString+ , getNumber+ , getList+ , getBool+ , isNull+ , mapNode+ , getConfig+ , pretty+ )+where++import Data.List (find)+import Data.List.Split (splitOn)+import Data.Map (Map)+import Data.Maybe (isJust)+import Data.String.Utils (join)++data Config =+ HOCONNode (Map String Config) |+ HOCONString String |+ HOCONNumber Double |+ HOCONList [Config] |+ HOCONBool Bool |+ HOCONNull+ deriving (Show, Eq)++isNull :: Config -> Bool+isNull HOCONNull = True+isNull _ = False++mapNode :: (Map String Config -> Config) -> Config -> Config+mapNode f (HOCONNode nodes) = f nodes+mapNode _ conf = conf++getNode :: String -> Config -> Maybe (Map String Config)+getNode key conf = do+ conf' <- getConfig key conf+ case conf' of+ HOCONNode nodes -> return nodes+ _ -> Nothing++getConfig :: String -> Config -> Maybe Config+getConfig key = go (splitOn "." key)+ where+ go [k] conf = do+ nodes <- getNodes conf+ nestedConf <- find ((== k) . fst) nodes+ return $ snd nestedConf+ go (k : ks) conf = do+ nodes <- getNodes conf+ nestedConf <- find ((== k) . fst) nodes+ go ks (snd nestedConf)++getNumber :: String -> Config -> Maybe Double+getNumber key conf = do+ conf' <- getConfig key conf+ case conf' of+ HOCONNumber n -> return n+ _ -> Nothing++getString :: String -> Config -> Maybe String+getString key conf = do+ conf' <- getConfig key conf+ case conf' of+ HOCONString s -> return s+ _ -> Nothing++getList :: String -> Config -> Maybe [Config]+getList key conf = do+ conf' <- getConfig key conf+ case conf' of+ HOCONList l -> return l+ _ -> Nothing++getBool :: String -> Config -> Maybe Bool+getBool key conf = do+ conf' <- getConfig key conf+ case conf' of+ HOCONBool b -> return b+ _ -> Nothing++hasPath :: String -> Config -> Bool+hasPath key = isJust . getConfig key++getNodes :: Config -> Maybe [(String, Config)]+getNodes (HOCONNode nodes) = Just nodes+getNodes _ = Nothing++pretty :: Config -> String+pretty HOCONNull = "null"+pretty (HOCONNumber n ) = show n+pretty (HOCONString s ) = "\"" ++ s ++ "\""+pretty (HOCONBool b ) = if b then "true" else "false"+pretty (HOCONList xs ) = "[" ++ (join "," . map pretty $ xs) ++ "]"+pretty (HOCONNode nodes) = "{" ++ (join "," . map (\(k, node) -> "\"" ++ k ++ "\":" ++ pretty node) $ nodes) ++ "}"
+ src/Data/Map.hs view
@@ -0,0 +1,24 @@+module Data.Map+ ( groupBy+ , sortByKey+ , Map+ )+where++import Data.List (sortBy)++type Map k v = [(k, v)]++groupBy :: (Ord k, Eq k) => (v -> k) -> [v] -> Map k [v]+groupBy f vs = sortBy (\(k1, _) (k2, _) -> compare k1 k2) $ go f (vs, [])+ where+ go :: Eq k => (v -> k) -> ([v], Map k [v]) -> Map k [v]+ go _ ([], vs) = vs+ go f (v : vs, acc) =+ let+ append key value (k, v) = if key == k then (k, value : v) else (k, v)+ acc' = if any (\(k', _) -> k' == f v) acc then map (append (f v) v) acc else (f v, [v]) : acc+ in go f (vs, acc')++sortByKey :: (Ord k) => Map k v -> Map k v+sortByKey = sortBy (\a b -> compare (fst a) (fst b))
+ src/Text/Parser/HOCON.hs view
@@ -0,0 +1,12 @@+module Text.Parser.HOCON+ ( parseHOCON+ , ParseError(..)+ )+where++import Data.HOCON (Config(..))+import Text.Parser.HOCON.Internal+import Text.ParserCombinators.Parsec (ParseError, parse)++parseHOCON :: String -> Either ParseError Config+parseHOCON = parse hoconParser "hocon" . preProcessing
+ src/Text/Parser/HOCON/Internal.hs view
@@ -0,0 +1,132 @@+-- Based off of [this gist for a JSON parser using Parsec](https://gist.github.com/fero23/51f63a33d733055d53b4)++{-# LANGUAGE ExistentialQuantification #-}++module Text.Parser.HOCON.Internal+ ( objectParser+ , stringParser+ , parseProps+ , parseLabel+ , arrayParser+ , numberParser+ , booleanParser+ , nullParser+ , preProcessing+ , hoconParser+ )+where++import Data.Bifunctor.Extra (mapValues)+import Data.HOCON (Config(..), mapNode)+import Data.List.Split (splitOn)+import Data.Map (groupBy, sortByKey)+import Data.String.Utils (replace, join, strip)+import Text.ParserCombinators.Parsec+ (char, Parser, alphaNum, digit, letter, noneOf, oneOf, space, string, between, sepBy, (<|>), many, many1, skipMany, try)++whitespace :: Parser ()+whitespace = skipMany space++objectParser :: Parser Config+objectParser = do+ whitespace >> char '{' >> whitespace+ props <- sepBy parseProps (whitespace >> (char ',' <|> char '\n') >> whitespace)+ whitespace >> char '}' >> whitespace+ return $ HOCONNode props++parseString :: Parser String+parseString = parseStr <|> parseOpenStr++parseStr :: Parser String+parseStr = between (char '\"') (char '\"') (many $ noneOf "\"" <|> try (string "\"\"" >> return '"'))++parseOpenStr :: Parser String+parseOpenStr = do+ l <- letter+ str <- many (alphaNum <|> oneOf "._-")+ return $ l : str++parseProps :: Parser (String, Config)+parseProps = do+ label <- parseString+ value <- whitespace >> (parseJsonLikeValue <|> parseObjectValue)+ return $ splitProp label value+ where+ parseJsonLikeValue = do+ (char ':' <|> char '=') >> whitespace+ objectParser <|> arrayParser <|> booleanParser <|> nullParser <|> stringParser <|> numberParser+ parseObjectValue = objectParser++parseLabel :: Parser String+parseLabel = do+ whitespace+ label <- parseString+ whitespace >> (char ':' <|> char '=') >> whitespace+ return label++numberParser :: Parser Config+numberParser = do+ whitespace+ digits <- many1 (digit <|> oneOf ".-")+ whitespace+ return . HOCONNumber $ read digits++stringParser :: Parser Config+stringParser = do+ whitespace+ str <- parseStr <|> parseOpenStr+ whitespace+ return $ HOCONString str++booleanParser :: Parser Config+booleanParser = do+ whitespace+ bool <- string "true" <|> string "false"+ whitespace+ return $ if bool == "true" then HOCONBool True else HOCONBool False++nullParser :: Parser Config+nullParser = do+ whitespace >> string "null" >> whitespace+ return HOCONNull++arrayParser :: Parser Config+arrayParser = do+ whitespace >> char '[' >> whitespace+ array <- sepBy+ (objectParser <|> arrayParser <|> booleanParser <|> nullParser <|> stringParser <|> numberParser)+ (whitespace >> char ',' >> whitespace)+ whitespace >> char ']' >> whitespace+ return $ HOCONList array++splitProp :: String -> Config -> (String, Config)+splitProp label value = case splitOn "." label of+ [l ] -> (l, value)+ (l : nested) -> (l, join nested value)+ where+ join [l ] value = HOCONNode [(l, value)]+ join (l : ls) value = HOCONNode [(l, join ls value)]+++preProcessing :: String -> String+preProcessing =+ replace "\n" "," . replace "{\n" "{" . replace "\n}" "}" . replace ",\n" "," . join "\n" . map strip . splitOn "\n"++hoconParser :: Parser Config+hoconParser = objectParser <|> do+ values <- sepBy parseProps (char ',')+ let grouped = groupAndMerge values+ let sorted = mapValues (mapNode (HOCONNode . sortByKey)) . sortByKey $ grouped+ return $ HOCONNode sorted+ where+ groupAndMerge values = do+ (key, mixedConfigsAndKeys) <- groupBy fst values+ let configs = map snd mixedConfigsAndKeys+ return (key, mergeAll configs)++mergeAll :: [Config] -> Config+mergeAll = foldl1 merge++merge :: Config -> Config -> Config+merge (HOCONNode a) (HOCONNode b) = HOCONNode (a ++ b)+
+ test/Data/MapSpec.hs view
@@ -0,0 +1,11 @@+module Data.MapSpec where++import Data.Map+import Test.Hspec++spec :: Spec+spec =+ describe "groupBy"+ $ it "joins all the items whose key is the same"+ $ groupBy length ["abc", "ab", "a", "atlas", "hades", "orpheus"]+ `shouldBe` [(1, ["a"]), (2, ["ab"]), (3, ["abc"]), (5, ["hades", "atlas"]), (7, ["orpheus"])]
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/Text/Parser/HOCON/InternalSpec.hs view
@@ -0,0 +1,74 @@+module Text.Parser.HOCON.InternalSpec+ ( spec+ )+where++import Data.HOCON (Config(..))+import Test.Hspec (describe, it, shouldBe, shouldSatisfy, Spec)+import Text.Parser.HOCON.Internal (arrayParser, numberParser, objectParser, parseProps, stringParser, hoconParser, preProcessing)+import Text.ParserCombinators.Parsec (parse)++isLeft :: Either a b -> Bool+isLeft (Left _) = True+isLeft _ = False++spec :: Spec+spec = do+ describe "stringParser" $ do+ it "succeeds" $ do+ parse stringParser "string" "\"saraza\"" `shouldBe` Right (HOCONString "saraza")+ parse stringParser "string" "saraza" `shouldBe` Right (HOCONString "saraza")+ it "fails" $ parse stringParser "string" "\"saraza" `shouldSatisfy` isLeft++ describe "numberParser" $ it "succeeds" $ do+ parse numberParser "number" "123456" `shouldBe` Right (HOCONNumber 123456)+ parse numberParser "number" "123.456" `shouldBe` Right (HOCONNumber 123.456)++ describe "arrayParser" $ do+ it "succeeds" $ do+ parse arrayParser "array" "[1,2,3]" `shouldBe` Right (HOCONList [HOCONNumber 1, HOCONNumber 2, HOCONNumber 3])+ parse arrayParser "array" "[1,\"a\", a]" `shouldBe` Right (HOCONList [HOCONNumber 1, HOCONString "a", HOCONString "a"])+ it "fails" $ do+ parse arrayParser "array" "[1" `shouldSatisfy` isLeft+ parse arrayParser "array" "[1, a?" `shouldSatisfy` isLeft++ describe "parseProps" $ it "succeeds" $ do+ parse parseProps "props" "foo: 123" `shouldBe` Right ("foo", HOCONNumber 123)+ parse parseProps "props" "foo = \"bar\"" `shouldBe` Right ("foo", HOCONString "bar")+ parse parseProps "props" "\"foo\":\"bar\"" `shouldBe` Right ("foo", HOCONString "bar")+ parse parseProps "props" "foo {\nbar = baz\n}" `shouldBe` Right ("foo", HOCONNode [("bar", HOCONString "baz")])+ parse parseProps "props" "foo.bar = baz" `shouldBe` Right ("foo", HOCONNode [("bar", HOCONString "baz")])++ describe "objectParser" $ do+ it "succeeds" $ do+ parse objectParser "object" "{foo = bar,\"baz\": \"baz\"}"+ `shouldBe` Right (HOCONNode [("foo", HOCONString "bar"), ("baz", HOCONString "baz")])+ parse objectParser "object" "{}" `shouldBe` Right (HOCONNode [])+ it "fails" $ do+ parse objectParser "object" "{\nfoo = bar\n\"baz\": \"baz\"\n" `shouldSatisfy` isLeft+ parse objectParser "object" "{\nfoo = bar\n\"baz\": \"baz\\n}" `shouldSatisfy` isLeft+ parse objectParser "object" "{" `shouldSatisfy` isLeft++ describe "preProcessing" $ do+ it "replaces al commas followed by newline with just the coma" $ do+ preProcessing "foo,\nbar,\nbaz" `shouldBe` "foo,bar,baz"+ preProcessing "foo, bar, baz" `shouldBe` "foo, bar, baz"+ it "replaces all opening brackets followed by newline with just the bracket" $ do+ preProcessing "{\nfoo = bar}" `shouldBe` "{foo = bar}"+ preProcessing "{foo = bar}" `shouldBe` "{foo = bar}"+ it "replaces all newlines followed by closing bracket with just the bracket" $ do+ preProcessing "{foo = bar\n}" `shouldBe` "{foo = bar}"+ preProcessing "{foo = bar}" `shouldBe` "{foo = bar}"+ preProcessing "{foo = bar\n }" `shouldBe` "{foo = bar}"++ describe "hoconParser" $ do+ it "succeeds" $ do+ parse hoconParser "hocon" "{}" `shouldBe` Right (HOCONNode [])+ parse hoconParser "hocon" "foo = bar" `shouldBe` Right (HOCONNode [("foo", HOCONString "bar")])+ parse hoconParser "hocon" "foo = bar,bar = 123"+ `shouldBe` Right (HOCONNode [("bar", HOCONNumber 123), ("foo", HOCONString "bar")])+ parse hoconParser "hocon" "foo = \"bar?!\",bar {baz = 123}"+ `shouldBe` Right (HOCONNode [("bar", HOCONNode [("baz", HOCONNumber 123)]), ("foo", HOCONString "bar?!")])+ it "fails" $ do+ parse hoconParser "hocon" "{" `shouldSatisfy` isLeft+ parse hoconParser "hocon" "foo =" `shouldSatisfy` isLeft
+ test/Text/Parser/HOCONSpec.hs view
@@ -0,0 +1,19 @@+module Text.Parser.HOCONSpec where++import Data.HOCON+import Test.Hspec+import Text.Parser.HOCON (parseHOCON)++isLeft :: Either a b -> Bool+isLeft (Left _) = True+isLeft _ = False++spec :: Spec+spec = describe "parseHOCON" $ do+ it "works" $ do+ parseHOCON "foo = bar" `shouldBe` Right (HOCONNode [("foo", HOCONString "bar")])+ parseHOCON "foo = bar\nbar = 123\nbaz = \"nani?!\""+ `shouldBe` Right (HOCONNode [("bar", HOCONNumber 123), ("baz", HOCONString "nani?!"), ("foo", HOCONString "bar")])+ it "fails" $ do+ parseHOCON "foo = bar\n{\n foo = bar\n}" `shouldSatisfy` isLeft+ parseHOCON "{" `shouldSatisfy` isLeft