packages feed

dhall-json 1.6.3 → 1.6.4

raw patch · 13 files changed

+726/−167 lines, 13 filesdep ~aesondep ~dhalldep ~tastyPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: aeson, dhall, tasty

API changes (from Hackage documentation)

+ Dhall.JSONToDhall: ArbitraryJSON :: Schema
+ Dhall.JSONToDhall: Bool :: Schema
+ Dhall.JSONToDhall: Double :: Schema
+ Dhall.JSONToDhall: Integer :: Schema
+ Dhall.JSONToDhall: List :: Schema -> Schema
+ Dhall.JSONToDhall: Natural :: Schema
+ Dhall.JSONToDhall: Optional :: Schema -> Schema
+ Dhall.JSONToDhall: Record :: RecordSchema -> Schema
+ Dhall.JSONToDhall: RecordSchema :: Map Text Schema -> RecordSchema
+ Dhall.JSONToDhall: Text :: Schema
+ Dhall.JSONToDhall: Union :: UnionSchema -> Schema
+ Dhall.JSONToDhall: UnionSchema :: Any -> UnionNumber -> Any -> UnionSchema
+ Dhall.JSONToDhall: [bool] :: UnionSchema -> Any
+ Dhall.JSONToDhall: [getRecordSchema] :: RecordSchema -> Map Text Schema
+ Dhall.JSONToDhall: [number] :: UnionSchema -> UnionNumber
+ Dhall.JSONToDhall: [text] :: UnionSchema -> Any
+ Dhall.JSONToDhall: data Schema
+ Dhall.JSONToDhall: data UnionSchema
+ Dhall.JSONToDhall: inferSchema :: Value -> Schema
+ Dhall.JSONToDhall: instance GHC.Base.Monoid Dhall.JSONToDhall.Schema
+ Dhall.JSONToDhall: instance GHC.Base.Monoid Dhall.JSONToDhall.UnionNumber
+ Dhall.JSONToDhall: instance GHC.Base.Monoid Dhall.JSONToDhall.UnionSchema
+ Dhall.JSONToDhall: instance GHC.Base.Semigroup Dhall.JSONToDhall.RecordSchema
+ Dhall.JSONToDhall: instance GHC.Base.Semigroup Dhall.JSONToDhall.Schema
+ Dhall.JSONToDhall: instance GHC.Base.Semigroup Dhall.JSONToDhall.UnionNumber
+ Dhall.JSONToDhall: instance GHC.Base.Semigroup Dhall.JSONToDhall.UnionSchema
+ Dhall.JSONToDhall: instance GHC.Classes.Eq Dhall.JSONToDhall.UnionNumber
+ Dhall.JSONToDhall: instance GHC.Classes.Eq Dhall.JSONToDhall.UnionSchema
+ Dhall.JSONToDhall: instance GHC.Classes.Ord Dhall.JSONToDhall.UnionNumber
+ Dhall.JSONToDhall: instance GHC.Enum.Bounded Dhall.JSONToDhall.UnionNumber
+ Dhall.JSONToDhall: newtype RecordSchema
+ Dhall.JSONToDhall: schemaToDhallType :: Schema -> Expr s a

Files

CHANGELOG.md view
@@ -1,3 +1,15 @@+1.6.4++* [Add `json-to-dhall` support for inferring the schema](https://github.com/dhall-lang/dhall-haskell/pull/1773)+    * You no longer need to provide the command with an explicit schema.  The+      command will infer a reasonably close schema from the provided JSON+* [Add `json-to-dhall type` subcommand](https://github.com/dhall-lang/dhall-haskell/pull/1776)+    * You can use this subcommand to print the inferred schema for a JSON value,+      so that you can edit the schema and use it for subsequent invocations.+* [Add `json-to-dhall` support for using `toMap`](https://github.com/dhall-lang/dhall-haskell/pull/1745)+    * Now if you specify a `Map` as the schema, the generated Dhall code will+      use `toMap` to improve the appearance+ 1.6.3  * [yaml: Single-quote date/bool string fields](https://github.com/dhall-lang/dhall-haskell/commits/master/dhall-json)
dhall-json.cabal view
@@ -1,6 +1,6 @@ Name: dhall-json-Version: 1.6.3-Cabal-Version: >=1.8.0.2+Version: 1.6.4+Cabal-Version: >=1.10 Build-Type: Simple Tested-With: GHC == 8.2.2, GHC == 8.4.3, GHC == 8.6.1 License: BSD3@@ -14,13 +14,14 @@     Use this package if you want to convert between Dhall expressions and JSON     or YAML. You can use this package as a library or an executable:     .-    * See the "Dhall.JSON" module if you want to use this package as a library+    * See the "Dhall.JSON" or "Dhall.JSONToDhall" modules if you want to use+      this package as a library     .-    * Use the @dhall-to-json@ or @dhall-to-yaml@ programs from this package if-      you want an executable+    * Use the @dhall-to-json@, @dhall-to-yaml@, or @json-to-dhall@ programs from+      this package if you want an executable     .-    The "Dhall.JSON" module also contains instructions for how to use this-    package+    The "Dhall.JSON" and "Dhall.JSONToDhall" modules also contains instructions+    for how to use this package Category: Compiler Extra-Source-Files:     CHANGELOG.md@@ -40,7 +41,7 @@         aeson-yaml                >= 1.0.6    && < 1.1 ,         bytestring                               < 0.11,         containers                                     ,-        dhall                     >= 1.31.0   && < 1.32,+        dhall                     >= 1.31.0   && < 1.33,         exceptions                >= 0.8.3    && < 0.11,         filepath                                 < 1.5 ,         optparse-applicative      >= 0.14.0.0 && < 0.16,@@ -57,6 +58,7 @@     Other-Modules:         Dhall.JSON.Util     GHC-Options: -Wall+    Default-Language: Haskell2010  Executable dhall-to-json     Hs-Source-Dirs: dhall-to-json@@ -73,6 +75,7 @@     Other-Modules:         Paths_dhall_json     GHC-Options: -Wall+    Default-Language: Haskell2010  Executable dhall-to-yaml     Hs-Source-Dirs: dhall-to-yaml@@ -83,6 +86,7 @@     Other-Modules:         Paths_dhall_json     GHC-Options: -Wall+    Default-Language: Haskell2010  Executable json-to-dhall     Hs-Source-Dirs: json-to-dhall@@ -104,6 +108,7 @@     Other-Modules:         Paths_dhall_json     GHC-Options: -Wall+    Default-Language: Haskell2010  Test-Suite tasty     Type: exitcode-stdio-1.0@@ -119,3 +124,4 @@         text              ,         tasty-hunit >= 0.2     GHC-Options: -Wall+    Default-Language: Haskell2010
json-to-dhall/Main.hs view
@@ -29,6 +29,7 @@ import qualified System.Console.ANSI                       as ANSI import qualified System.Exit import qualified System.IO                                 as IO+import qualified Dhall.Core import qualified Dhall.Pretty import qualified Paths_dhall_json                          as Meta @@ -46,22 +47,29 @@  -- | All the command arguments and options data Options-    = Options-        { schema     :: Text+    = Default+        { schema     :: Maybe Text         , conversion :: Conversion         , file       :: Maybe FilePath         , output     :: Maybe FilePath         , ascii      :: Bool         , plain      :: Bool         }+    | Type+        { file       :: Maybe FilePath+        , output     :: Maybe FilePath+        , ascii      :: Bool+        , plain      :: Bool+        }     | Version     deriving Show  -- | Parser for all the command arguments and options parseOptions :: Parser Options parseOptions =-        (   Options-        <$> parseSchema+        typeCommand+    <|> (   Default+        <$> optional parseSchema         <*> parseConversion         <*> optional parseFile         <*> optional parseOutput@@ -70,10 +78,24 @@         )     <|> parseVersion   where+    typeCommand =+        Options.hsubparser+            (Options.command "type" info <> Options.metavar "type")+      where+        info =+            Options.info parser (Options.progDesc "Output the inferred Dhall type from a JSON value")++        parser =+                Type+            <$> optional parseFile+            <*> optional parseOutput+            <*> parseASCII+            <*> parsePlain+     parseSchema =         Options.strArgument             (  Options.metavar "SCHEMA"-            <> Options.help "Dhall type expression (schema)"+            <> Options.help "Dhall type (schema).  You can omit the schema to let the executable infer the schema from the JSON value."             )      parseVersion =@@ -120,52 +142,76 @@      options <- Options.execParser parserInfo -    case options of-        Version -> do-            putStrLn (showVersion Meta.version)+    let toCharacterSet ascii = case ascii of+            True  -> ASCII+            False -> Unicode -        Options {..} -> do-            let characterSet = case ascii of-                    True  -> ASCII-                    False -> Unicode+    let toValue file = do+            bytes <- case file of+                Nothing   -> ByteString.getContents+                Just path -> ByteString.readFile path -            handle $ do-                bytes <- case file of-                    Nothing   -> ByteString.getContents-                    Just path -> ByteString.readFile path+            case Aeson.eitherDecode bytes of+                Left err -> throwIO (userError err)+                Right v -> pure v -                value :: Aeson.Value <- case Aeson.eitherDecode bytes of-                  Left err -> throwIO (userError err)-                  Right v -> pure v+    let toSchema schema value = do+            finalSchema <- case schema of+                Just text -> resolveSchemaExpr text+                Nothing   -> return (schemaToDhallType (inferSchema value)) -                expr <- typeCheckSchemaExpr id =<< resolveSchemaExpr schema+            typeCheckSchemaExpr id finalSchema -                result <- case dhallFromJSON conversion expr value of-                  Left err     -> throwIO err-                  Right result -> return result+    let renderExpression characterSet plain output expression = do+            let document =+                    Dhall.Pretty.prettyCharacterSet characterSet expression -                let document = Dhall.Pretty.prettyCharacterSet characterSet result+            let stream = Dhall.Pretty.layout document -                let stream = Dhall.Pretty.layout document+            case output of+                Nothing -> do+                    supportsANSI <- ANSI.hSupportsANSI IO.stdout -                case output of-                    Nothing -> do-                        supportsANSI <- ANSI.hSupportsANSI IO.stdout+                    let ansiStream =+                            if supportsANSI && not plain+                            then fmap Dhall.Pretty.annToAnsiStyle stream+                            else Pretty.unAnnotateS stream -                        let ansiStream =-                                if supportsANSI && not plain-                                then fmap Dhall.Pretty.annToAnsiStyle stream-                                else Pretty.unAnnotateS stream+                    Pretty.Terminal.renderIO IO.stdout ansiStream -                        Pretty.Terminal.renderIO IO.stdout ansiStream+                    Text.IO.putStrLn "" -                        Text.IO.putStrLn ""+                Just file_ ->+                    IO.withFile file_ IO.WriteMode $ \h -> do+                        Pretty.Text.renderIO h stream -                    Just file_ ->-                        IO.withFile file_ IO.WriteMode $ \h -> do-                            Pretty.Text.renderIO h stream+                        Text.IO.hPutStrLn h "" -                            Text.IO.hPutStrLn h ""+    case options of+        Version -> do+            putStrLn (showVersion Meta.version)++        Default{..} -> do+            let characterSet = toCharacterSet ascii++            handle $ do+                value <- toValue file++                finalSchema <- toSchema schema value++                expression <- Dhall.Core.throws (dhallFromJSON conversion finalSchema value)++                renderExpression characterSet plain output expression++        Type{..} -> do+            let characterSet = toCharacterSet ascii++            handle $ do+                value <- toValue file++                finalSchema <- toSchema Nothing value++                renderExpression characterSet plain output finalSchema  handle :: IO a -> IO a handle = Control.Exception.handle handler
src/Dhall/JSON.hs view
@@ -1147,7 +1147,7 @@  >>> :set -XOverloadedStrings >>> import Core->>> Dhall.JSON.codeToValue "(stdin)" "{ a = 1 }"+>>> Dhall.JSON.codeToValue defaultConversion ForbidWithinJSON Nothing "{ a = 1 }" >>> Object (fromList [("a",Number 1.0)]) -} codeToValue@@ -1158,7 +1158,7 @@   -> Text  -- ^ Input text.   -> IO Value codeToValue conversion specialDoubleMode mFilePath code = do-    parsedExpression <- Core.throws (Dhall.Parser.exprFromText (fromMaybe "(stdin)" mFilePath) code)+    parsedExpression <- Core.throws (Dhall.Parser.exprFromText (fromMaybe "(input)" mFilePath) code)      let rootDirectory = case mFilePath of             Nothing -> "."
src/Dhall/JSONToDhall.hs view
@@ -8,150 +8,240 @@ {-# LANGUAGE PatternSynonyms     #-} {-# LANGUAGE ScopedTypeVariables #-} -{-| Convert JSON data to Dhall given a Dhall /type/ expression necessary to make the translation unambiguous.+{-| Convert JSON data to Dhall in one of two ways: -    Reasonable requirements for conversion are:+    * By default, the conversion will make a best-effort at inferring the+      corresponding Dhall type -    1. The Dhall type expression @/t/@ passed as an argument to @json-to-dhall@ should be a valid type of the resulting Dhall expression-    2. A JSON data produced by the corresponding @dhall-to-json@ from the Dhall expression of type @/t/@ should (under reasonable assumptions) reproduce the original Dhall expression using @json-to-dhall@ with type argument @/t/@+    * Optionally, you can specify an expected Dhall type necessary to make the+      translation unambiguous. -    Only a subset of Dhall types consisting of all the primitive types as well as @Optional@, @Union@ and @Record@ constructs, is used for reading JSON data:+    Either way, if you supply the generated Dhall result to @dhall-to-json@ you+    should get back the original JSON. -    * @Bool@s-    * @Natural@s-    * @Integer@s-    * @Double@s-    * @Text@s-    * @List@s-    * @Optional@ values+    Only a subset of Dhall types are supported when converting from JSON:++    * @Bool@+    * @Natural@+    * @Integer@+    * @Double@+    * @Text@+    * @List@+    * @Optional@     * unions     * records+    * @Prelude.Type.Map@+    * @Prelude.Type.JSON@ - You can always convert JSON data to this type as a+      last resort if you don't know the schema in advance. -    Additionally, you can read in arbitrary JSON data into a Dhall value of-    type @https://prelude.dhall-lang.org/JSON/Type@ if you don't know the-    schema of the JSON data in advance.+    You can use this code as a library (this module) or as an executable+    named @json-to-dhall@, which is used in the examples below. -    This library can be used to implement an executable which takes any data-    serialisation format which can be parsed as an Aeson @Value@ and converts-    the result to a Dhall value. One such executable is @json-to-dhall@ which-    is used in the examples below.+    By default the @json-to-dhall@ executable attempts to infer the+    appropriate Dhall type from the JSON data, like this: +> $ json-to-dhall <<< 1+> 1++    ... but you can also provide an explicit schema on the command line if you+    prefer a slightly different Dhall type which still represents the same JSON+    value:++> $ json-to-dhall Integer <<< 1+> +1++    You can also get the best of both worlds by using the @type@ subcommand to+    infer the schema:++> $ json-to-dhall type <<< '[ "up", "down" ]' | tee schema.dhall+> List Text++    ... and then edit the @./schema.dhall@ file to better match the type you+    intended, such as:++> $ $EDITOR schema.dhall+> $ cat ./schema.dhall+> List < up | down >++    ... and then use the edited schema for subsequent conversions:++> $ json-to-dhall ./schema.dhall <<< '[ "up", "down" ]'+> [ < down | up >.up, < down | up >.down ]+ == Primitive types      JSON @Bool@s translate to Dhall bools: -> $ json-to-dhall Bool <<< 'true'+> $ json-to-dhall <<< 'true' > True-> $ json-to-dhall Bool <<< 'false'+> $ json-to-dhall <<< 'false' > False      JSON numbers translate to Dhall numbers: -> $ json-to-dhall Integer <<< 2-> +2+> $ json-to-dhall <<< 2+> 2+> $ json-to-dhall <<< -2+> -2+> $ json-to-dhall <<< -2.1+> -2.1 > $ json-to-dhall Natural <<< 2 > 2-> $ json-to-dhall Double <<< -2.345-> -2.345+> $ json-to-dhall Integer <<< 2+> +2+> $ json-to-dhall Double <<< 2+> 2.0 -    Dhall @Text@ corresponds to JSON text:+    JSON text corresponds to Dhall @Text@ by default: -> $ json-to-dhall Text <<< '"foo bar"'+> $ json-to-dhall <<< '"foo bar"' > "foo bar" +    ... but you can also decode text into a more structured enum, too, if you+    provide an explicit schema: +> $ json-to-dhall '< A | B >' <<< '"A"'+> < A | B >.A+ == Lists and records      Dhall @List@s correspond to JSON lists: -> $ json-to-dhall 'List Integer' <<< '[1, 2, 3]'-> [ +1, +2, +3 ]+> $ json-to-dhall <<< '[ 1, 2, 3 ]'+> [ 1, 2, 3 ] +    You can even decode an empty JSON list to Dhall: -    Dhall __records__ correspond to JSON records:+> $ json-to-dhall <<< '[]'+> [] : List <> -> $ json-to-dhall '{foo : List Integer}' <<< '{"foo": [1, 2, 3]}'-> { foo = [ +1, +2, +3 ] }+    ... which will infer the empty @\<\>@ type if there are no other constraints+    on the type.  If you provide an explicit type annotation then the conversion+    will use that instead: +> $ json-to-dhall 'List Natural' <<< '[]'+> [] : List Natural -    Note, that by default, only the fields required by the Dhall type argument are parsed (as you commonly will not need all the data), the remaining ones being ignored:+    Dhall records correspond to JSON records: -> $ json-to-dhall '{foo : List Integer}' <<< '{"foo": [1, 2, 3], "bar" : "asdf"}'-> { foo = [ +1, +2, +3 ] }+> $ json-to-dhall <<< '{ "foo": [ 1, 2, 3 ] }'+> { foo = [ 1, 2, 3 ] } +    If you specify a schema with additional @Optional@ fields then they will be+    @None@ if absent: -    If you do need to make sure that Dhall fully reflects JSON record data comprehensively, @--records-strict@ flag should be used:+> $ json-to-dhall '{ foo : List Natural, bar : Optional Bool }' <<< '{ "foo": [ 1, 2, 3 ] }'+> { bar = None Bool, foo = [ 1, 2, 3 ] } -> $ json-to-dhall --records-strict '{foo : List Integer}' <<< '{"foo": [1, 2, 3], "bar" : "asdf"}'-> Error: Key(s) @bar@ present in the JSON object but not in the corresponding Dhall record. This is not allowed in presence of --records-strict:+    ... and @Some@ if present: +> $ json-to-dhall '{ foo : List Natural, bar : Optional Bool }' <<< '{ "foo": [ 1, 2, 3 ], "bar": true }'+> { bar = Some True, foo = [ 1, 2, 3 ] } -    By default, JSON key-value arrays will be converted to Dhall records:+    If you specify a schema with too few fields, then the behavior is+    configurable.  By default, the conversion will reject extra fields: -> $ json-to-dhall '{ a : Integer, b : Text }' <<< '[{"key":"a", "value":1}, {"key":"b", "value":"asdf"}]'-> { a = +1, b = "asdf" }+> $ json-to-dhall '{ foo : List Natural }' <<< '{ "foo": [ 1, 2, 3 ], "bar": true }'+>+> Error: Key(s) bar present in the JSON object but not in the expected Dhall record type. This is not allowed unless you enable the --records-loose flag:+>+> Expected Dhall type:+> { foo : List Natural }+>+> JSON:+> {+>     "foo": [+>         1,+>         2,+>         3+>     ],+>     "bar": true+> } +  ... as the error message suggests, extra fields are ignored if you enable the+  @--records-loose@ flag. -    Attempting to do the same with @--no-keyval-arrays@ on will result in error:+> $ json-to-dhall --records-loose '{ foo : List Natural }' <<< '{ "foo": [ 1, 2, 3 ], "bar": true }'+> { foo = [ 1, 2, 3 ] } -> $ json-to-dhall --no-keyval-arrays '{ a : Integer, b : Text }' <<< '[{"key":"a", "value":1}, {"key":"b", "value":"asdf"}]'+    You can convert JSON key-value arrays to Dhall records, but only if you+    supply an explicit Dhall type:++> $ json-to-dhall '{ a : Natural, b : Text }' <<< '[ { "key": "a", "value": 1 }, { "key": "b", "value": "asdf" } ]'+> { a = 1, b = "asdf" }++    You can also disable this behavior using the @--no-keyval-arrays@:++> $ json-to-dhall --no-keyval-arrays '{ a : Natural, b : Text }' <<< '[ { "key": "a", "value": 1 }, { "key": "b", "value": "asdf" } ]' > Error: JSON (key-value) arrays cannot be converted to Dhall records under --no-keyval-arrays flag: -    Conversion of the homogeneous JSON maps to the corresponding Dhall association lists by default:+    You can also convert JSON records to Dhall @Map@s, but only if you supply an+    explicit schema: -> $ json-to-dhall 'List { mapKey : Text, mapValue : Text }' <<< '{"foo": "bar"}'-> [ { mapKey = "foo", mapValue = "bar" } ]+> $ json-to-dhall 'List { mapKey : Text, mapValue : Text }' <<< '{ "foo": "bar" }'+> toMap { foo = "bar" }      The map keys can even be union types instead of `Text`: -> $ json-to-dhall 'List { mapKey : < A | B >, mapValue : Natural }' <<< '{"A": 1, "B": 2}'+> $ json-to-dhall 'List { mapKey : < A | B >, mapValue : Natural }' <<< '{ "A": 1, "B": 2 }' > [ { mapKey = < A | B >.A, mapValue = 1 }, { mapKey = < A | B >.B, mapValue = 2 } ] -    Flag @--no-keyval-maps@ switches off this mechanism (if one would ever need it):+    You can similarly disable this feature using @--no-keyval-maps@: -> $ json-to-dhall --no-keyval-maps 'List { mapKey : Text, mapValue : Text }' <<< '{"foo": "bar"}'+> $ json-to-dhall --no-keyval-maps 'List { mapKey : Text, mapValue : Text }' <<< '{ "foo": "bar" }' > Error: Homogeneous JSON map objects cannot be converted to Dhall association lists under --no-keyval-arrays flag   == Optional values and unions -    Dhall @Optional@ Dhall type allows null or missing JSON values:+    JSON @null@ values correspond to @Optional@ Dhall values: -> $ json-to-dhall "Optional Integer" <<< '1'-> Some +1+> $ json-to-dhall <<< 'null'+> None <> -> $ json-to-dhall "Optional Integer" <<< null-> None Integer+    ... and the schema inference logic will automatically wrap other values in+    @Optional@ to ensure that the types line up: -> $ json-to-dhall '{ a : Integer, b : Optional Text }' <<< '{ "a": 1 }'-{ a = +1, b = None Text }+> $ json-to-dhall <<< '[ 1, null ]'+> [ Some 1, None Natural ] +    A field that might be absent also corresponds to an @Optional@ type: +> $ json-to-dhall <<< '[ { "x": 1 }, { "x": 2, "y": true } ]'+> [ { x = 1, y = None Bool }, { x = 2, y = Some True } ] -    For Dhall __union types__ the correct value will be based on matching the type of JSON expression:+    For Dhall union types the correct value will be based on matching the type+    of JSON expression if you give an explicit type:  > $ json-to-dhall 'List < Left : Text | Right : Integer >' <<< '[1, "bar"]' > [ < Left : Text | Right : Integer >.Right +1-  , < Left : Text | Right : Integer >.Left "bar"-  ]+> , < Left : Text | Right : Integer >.Left "bar"+> ] -> $ json-to-dhall '{foo : < Left : Text | Right : Integer >}' <<< '{ "foo": "bar" }'-> { foo = < Left : Text | Right : Integer >.Left "bar" }+    Also, the schema inference logic will still infer a union anyway in order+    to reconcile simple types: -    In presence of multiple potential matches, the first will be selected by default:+> $ json-to-dhall <<< '[ 1, true ]'+> [ < Bool : Bool | Natural : Natural >.Natural 1+> , < Bool : Bool | Natural : Natural >.Bool True+> ] +    In presence of multiple potential matches, the first will be selected by+    default:+ > $ json-to-dhall '{foo : < Left : Text | Middle : Text | Right : Integer >}' <<< '{ "foo": "bar"}' > { foo = < Left : Text | Middle : Text | Right : Integer >.Left "bar" } -    This will result in error if @--unions-strict@ flag is used, with the list of alternative matches being reported (as a Dhall list)+    This will result in error if @--unions-strict@ flag is used, with the list+    of alternative matches being reported (as a Dhall list)  > $ json-to-dhall --unions-strict '{foo : < Left : Text | Middle : Text | Right : Integer >}' <<< '{ "foo": "bar"}' > Error: More than one union component type matches JSON value > ... > Possible matches:-< Left : Text | Middle : Text | Right : Integer >.Left "bar"+> < Left : Text | Middle : Text | Right : Integer >.Left "bar" > ---------< Left : Text | Middle : Text | Right : Integer >.Middle "bar"+> < Left : Text | Middle : Text | Right : Integer >.Middle "bar"  == Weakly-typed JSON @@ -171,9 +261,11 @@ > → λ(null : JSON) > → array >   [ object->     [ { mapKey = "foo", mapValue = null }->     , { mapKey = "bar", mapValue = array [ number 1.0, bool True ] }->     ]+>     ( toMap+>         { bar = array [ number 1.0, bool True ]+>         , foo = null+>         }+>     ) >   ]  You can also mix and match JSON fields whose schemas are known or unknown:@@ -199,6 +291,36 @@ >   } > ] +    The schema inference algorithm will also infer this schema of last resort+    when unifying a simple type with a record or a list:++> $ json-to-dhall <<< '[ 1, [] ]'+> [ λ(JSON : Type) →+>   λ ( json+>     : { array : List JSON → JSON+>       , bool : Bool → JSON+>       , double : Double → JSON+>       , integer : Integer → JSON+>       , null : JSON+>       , object : List { mapKey : Text, mapValue : JSON } → JSON+>       , string : Text → JSON+>       }+>     ) →+>     json.integer +1+> , λ(JSON : Type) →+>   λ ( json+>     : { array : List JSON → JSON+>       , bool : Bool → JSON+>       , double : Double → JSON+>       , integer : Integer → JSON+>       , null : JSON+>       , object : List { mapKey : Text, mapValue : JSON } → JSON+>       , string : Text → JSON+>       }+>     ) →+>     json.array ([] : List JSON)+> ]+ -}  module Dhall.JSONToDhall (@@ -210,6 +332,13 @@     , typeCheckSchemaExpr     , dhallFromJSON +    -- * Schema inference+    , Schema(..)+    , RecordSchema(..)+    , UnionSchema(..)+    , inferSchema+    , schemaToDhallType+     -- * Exceptions     , CompileError(..)     , showCompileError@@ -218,17 +347,22 @@ import           Control.Applicative ((<|>)) import           Control.Exception (Exception, throwIO) import           Control.Monad.Catch (throwM, MonadCatch)+import           Data.Aeson (Value) import qualified Data.Aeson as A import           Data.Aeson.Encode.Pretty (encodePretty) import qualified Data.ByteString.Lazy.Char8 as BSL8 import           Data.Either (rights) import           Data.Foldable (toList)+import qualified Data.Foldable as Foldable import qualified Data.HashMap.Strict as HM import           Data.List ((\\)) import qualified Data.List as List-import           Data.Monoid ((<>))+import qualified Data.Map+import qualified Data.Map.Merge.Lazy as Data.Map.Merge+import           Data.Monoid (Any(..)) import qualified Data.Ord as Ord import           Data.Scientific (floatingOrInteger, toRealFloat)+import           Data.Semigroup (Semigroup(..)) import qualified Data.Sequence as Seq import qualified Data.String import qualified Data.Text as Text@@ -242,7 +376,9 @@ import qualified Dhall.Core as D import           Dhall.Core (Expr(App), Chunks(..), DhallDouble(..)) import qualified Dhall.Import+import qualified Dhall.Lint as Lint import qualified Dhall.Map as Map+import qualified Dhall.Optics as Optics import qualified Dhall.Parser import           Dhall.Parser (Src) import qualified Dhall.TypeCheck as D@@ -365,13 +501,278 @@       D.Const D.Type -> return expr       _              -> throwM . compileException $ BadDhallType t expr -keyValMay :: A.Value -> Maybe (Text, A.Value)+keyValMay :: Value -> Maybe (Text, Value) keyValMay (A.Object o) = do      A.String k <- HM.lookup "key" o      v <- HM.lookup "value" o      return (k, v) keyValMay _ = Nothing +{-| Given a JSON `Value`, make a best-effort guess of what the matching Dhall+    type should be++    This is used by @{json,yaml}-to-dhall@ if the user does not supply a schema+    on the command line+-}+inferSchema :: Value -> Schema+inferSchema (A.Object m) =+    let convertMap = Data.Map.fromList . HM.toList++    in (Record . RecordSchema . convertMap) (fmap inferSchema m)+inferSchema (A.Array xs) =+    List (Foldable.foldMap inferSchema xs)+inferSchema (A.String _) =+    Text+inferSchema (A.Number n) =+    case floatingOrInteger n of+        Left (_ :: Double) -> Double+        Right (integer :: Integer)+            | 0 <= integer -> Natural+            | otherwise    -> Integer+inferSchema (A.Bool _) =+    Bool+inferSchema A.Null =+    Optional mempty++-- | A record type that `inferSchema` can infer+newtype RecordSchema =+    RecordSchema { getRecordSchema :: Data.Map.Map Text Schema }++instance Semigroup RecordSchema where+    RecordSchema l <> RecordSchema r = RecordSchema m+      where+        -- The reason this is not @Just (Optional s)@ is to avoid creating a+        -- double `Optional` wrapper when unifying a @null@ field with an+        -- absent field.+        onMissing _ s = Just (s <> Optional mempty)++        m = Data.Map.Merge.merge+                (Data.Map.Merge.mapMaybeMissing onMissing)+                (Data.Map.Merge.mapMaybeMissing onMissing)+                (Data.Map.Merge.zipWithMatched (\_ -> (<>)))+                l+                r++recordSchemaToDhallType :: RecordSchema -> Expr s a+recordSchemaToDhallType (RecordSchema m) =+    D.Record (Map.fromList (Data.Map.toList (fmap schemaToDhallType m)))++{-| `inferSchema` will never infer a union type with more than one numeric+    alternative++    Instead, the most general alternative type will be preferred, which this+    type tracks+-}+data UnionNumber+    = UnionAbsent+    -- ^ The union type does not have a numeric alternative+    | UnionNatural+    -- ^ The union type has a @Natural@ alternative+    | UnionInteger+    -- ^ The union type has an @Integer@ alternative+    | UnionDouble+    -- ^ The union type has a @Double@ alternative+    deriving (Bounded, Eq, Ord)++-- | Unify two numeric alternative types by preferring the most general type+instance Semigroup UnionNumber where+    (<>) = max++instance Monoid UnionNumber where+    mempty = minBound++    mappend = (<>)++unionNumberToAlternatives :: UnionNumber -> [ (Text, Maybe (Expr s a)) ]+unionNumberToAlternatives UnionAbsent  = []+unionNumberToAlternatives UnionNatural = [ ("Natural", Just D.Natural) ]+unionNumberToAlternatives UnionInteger = [ ("Integer", Just D.Integer) ]+unionNumberToAlternatives UnionDouble  = [ ("Double" , Just D.Double ) ]++{-| A union type that `inferSchema` can infer++    This type will have at most three alternatives:++    * A @Bool@ alternative+    * Either a @Natural@, @Integer@, or @Double@ alternative+    * A @Text@ alternative++    These alternatives will always use the same names and types when we convert+    back to a Dhall type, so we only need to keep track of whether or not each+    alternative is present.++    We only store simple types inside of a union since we treat any attempt to+    unify a simple type with a complex type as a strong indication that the+    user intended for the schema to be `ArbitraryJSON`.+-}+data UnionSchema = UnionSchema+    { bool :: Any+    -- ^ `True` if the union has a @Bool@ alternative+    , number :: UnionNumber+    -- ^ Up to one numeric alternative+    , text :: Any+    -- ^ `True` if the union has a @Text@ alternative+    } deriving (Eq)++unionSchemaToDhallType :: UnionSchema -> Expr s a+unionSchemaToDhallType UnionSchema{..} = D.Union (Map.fromList alternatives)+  where+    alternatives =+            (if getAny bool then [ ("Bool", Just D.Bool) ] else [])+        <>  unionNumberToAlternatives number+        <>  (if getAny text then [ ("Text", Just D.Text) ] else [])++-- | Unify two union types by combining their alternatives+instance Semigroup UnionSchema where+    UnionSchema boolL numberL textL <> UnionSchema boolR numberR textR =+        UnionSchema{..}+      where+        bool = boolL <> boolR++        number = numberL <> numberR++        text = textL <> textR++instance Monoid UnionSchema where+    mempty = UnionSchema{..}+      where+        bool = mempty++        number = mempty++        text = mempty++    mappend = (<>)++{-| A `Schema` is a subset of the `Expr` type representing all possible+    Dhall types that `inferSchema` could potentially return+-}+data Schema+    = Bool+    | Natural+    | Integer+    | Double+    | Text+    | List Schema+    | Optional Schema+    | Record RecordSchema+    | Union UnionSchema+    | ArbitraryJSON++-- | (`<>`) unifies two schemas+instance Semigroup Schema where+    -- `ArbitraryJSON` subsumes every other type+    ArbitraryJSON <> _ = ArbitraryJSON+    _ <> ArbitraryJSON = ArbitraryJSON++    -- Simple types unify with themselves+    Bool    <> Bool    = Bool+    Text    <> Text    = Text+    Natural <> Natural = Natural+    Integer <> Integer = Integer+    Double  <> Double  = Double++    -- Complex types unify with themselves+    Record   l <> Record   r = Record   (l <> r)+    List     l <> List     r = List     (l <> r)+    Union    l <> Union    r = Union    (l <> r)+    Optional l <> Optional r = Optional (l <> r)++    -- Numeric types unify on the most general numeric type+    Natural <> Integer = Integer+    Integer <> Natural = Integer+    Natural <> Double  = Double+    Integer <> Double  = Double+    Double  <> Natural = Double+    Double  <> Integer = Double++    -- Unifying two different simple types produces a union+    Bool    <> Natural = Union mempty{ bool = Any True, number = UnionNatural }+    Bool    <> Integer = Union mempty{ bool = Any True, number = UnionInteger }+    Bool    <> Double  = Union mempty{ bool = Any True, number = UnionDouble }+    Bool    <> Text    = Union mempty{ bool = Any True, text = Any True }+    Natural <> Bool    = Union mempty{ bool = Any True, number = UnionNatural }+    Natural <> Text    = Union mempty{ number = UnionNatural, text = Any True }+    Integer <> Bool    = Union mempty{ bool = Any True, number = UnionInteger }+    Integer <> Text    = Union mempty{ number = UnionInteger, text = Any True }+    Double  <> Bool    = Union mempty{ bool = Any True, number = UnionDouble }+    Double  <> Text    = Union mempty{ number = UnionDouble, text = Any True }+    Text    <> Bool    = Union mempty{ bool = Any True, text = Any True }+    Text    <> Natural = Union mempty{ number = UnionNatural, text = Any True }+    Text    <> Integer = Union mempty{ number = UnionInteger, text = Any True }+    Text    <> Double  = Union mempty{ number = UnionDouble, text = Any True }++    -- The empty union type is the identity of unification+    Union l <> r | l == mempty = r+    l <> Union r | r == mempty = l++    -- Unifying a simple type with a union adds the simple type as yet another+    -- alternative+    Bool    <> Union r = Union (mempty{ bool   = Any True } <> r)+    Natural <> Union r = Union (mempty{ number = UnionNatural } <> r)+    Integer <> Union r = Union (mempty{ number = UnionInteger } <> r)+    Double  <> Union r = Union (mempty{ number = UnionDouble} <> r)+    Text    <> Union r = Union (mempty{ text   = Any True } <> r)+    Union l <> Bool    = Union (l <> mempty{ bool   = Any True })+    Union l <> Natural = Union (l <> mempty{ number = UnionNatural })+    Union l <> Integer = Union (l <> mempty{ number = UnionInteger })+    Union l <> Double  = Union (l <> mempty{ number = UnionDouble })+    Union l <> Text    = Union (l <> mempty{ text   = Any True })++    -- All of the remaining cases are for unifying simple types with+    -- complex types.  The only such case that can be sensibly unified is for+    -- `Optional`++    -- `Optional` subsumes every type other than `ArbitraryJSON`+    Optional l <> r = Optional (l <> r)+    l <> Optional r = Optional (l <> r)++    -- For all other cases, a simple type cannot be unified with a complex+    -- type, so fall back to `ArbitraryJSON`+    --+    -- This is equivalent to:+    --+    --     _ <> _ = ArbitraryJSON+    --+    -- ... but more explicit, in order to minimize the chance of ignoring an+    -- important case by accident.+    List _   <> _        = ArbitraryJSON+    _        <> List _   = ArbitraryJSON+    Record _ <> _        = ArbitraryJSON+    _        <> Record _ = ArbitraryJSON++instance Monoid Schema where+    mempty = Union mempty++    mappend = (<>)++-- | Convert a `Schema` to the corresponding Dhall type+schemaToDhallType :: Schema -> Expr s a+schemaToDhallType Bool = D.Bool+schemaToDhallType Natural = D.Natural+schemaToDhallType Integer = D.Integer+schemaToDhallType Double = D.Double+schemaToDhallType Text = D.Text+schemaToDhallType (List a) = D.App D.List (schemaToDhallType a)+schemaToDhallType (Optional a) = D.App D.Optional (schemaToDhallType a)+schemaToDhallType (Record r) = recordSchemaToDhallType r+schemaToDhallType (Union u) = unionSchemaToDhallType u+schemaToDhallType ArbitraryJSON =+    D.Pi "_" (D.Const D.Type)+        (D.Pi "_"+            (D.Record+                [ ("array" , D.Pi "_" (D.App D.List (V 0)) (V 1))+                , ("bool"  , D.Pi "_" D.Bool (V 1))+                , ("double", D.Pi "_" D.Double (V 1))+                , ("integer", D.Pi "_" D.Integer (V 1))+                , ("null"  , V 0)+                , ("object", D.Pi "_" (D.App D.List (D.Record [ ("mapKey", D.Text), ("mapValue", V 0)])) (V 1))+                , ("string", D.Pi "_" D.Text (V 1))+                ]+            )+            (V 1)+        )+ {-| The main conversion function. Traversing\/zipping Dhall /type/ and Aeson value trees together to produce a Dhall /term/ tree, given 'Conversion' options:  >>> :set -XOverloadedStrings@@ -387,9 +788,9 @@  -} dhallFromJSON-  :: Conversion -> ExprX -> A.Value -> Either CompileError ExprX+  :: Conversion -> ExprX -> Value -> Either CompileError ExprX dhallFromJSON (Conversion {..}) expressionType =-    loop (D.alphaNormalize (D.normalize expressionType))+    fmap (Optics.rewriteOf D.subExpressions Lint.useToMap) . loop (D.alphaNormalize (D.normalize expressionType))   where     -- any ~> Union     loop t@(D.Union tm) v = do@@ -435,7 +836,7 @@     -- key-value list ~> Record     loop t@(D.Record _) v@(A.Array a)         | not noKeyValArr-        , os :: [A.Value] <- toList a+        , os :: [Value] <- toList a         , Just kvs <- traverse keyValMay os         = loop t (A.Object $ HM.fromList kvs)         | noKeyValArr@@ -689,7 +1090,7 @@ -- ----------  red, purple, green-    :: (Monoid a, Data.String.IsString a) => a -> a+    :: (Semigroup a, Data.String.IsString a) => a -> a red    s = "\ESC[1;31m" <> s <> "\ESC[0m" -- bold purple s = "\ESC[1;35m" <> s <> "\ESC[0m" -- bold green  s = "\ESC[0;32m" <> s <> "\ESC[0m" -- plain@@ -697,7 +1098,7 @@ showExpr :: ExprX   -> String showExpr dhall = Text.unpack (D.pretty dhall) -showJSON :: A.Value -> String+showJSON :: Value -> String showJSON value = BSL8.unpack (encodePretty value)  data CompileError@@ -709,22 +1110,22 @@   -- generic mismatch (fallback)   | Mismatch       ExprX   -- Dhall expression-      A.Value -- Aeson value+      Value -- Aeson value   -- record specific-  | MissingKey     Text  ExprX A.Value-  | UnhandledKeys [Text] ExprX A.Value-  | NoKeyValArray        ExprX A.Value-  | NoKeyValMap          ExprX A.Value+  | MissingKey     Text  ExprX Value+  | UnhandledKeys [Text] ExprX Value+  | NoKeyValArray        ExprX Value+  | NoKeyValMap          ExprX Value   -- union specific   | ContainsUnion        ExprX-  | UndecidableUnion     ExprX A.Value [ExprX]+  | UndecidableUnion     ExprX Value [ExprX]  instance Show CompileError where     show = showCompileError "JSON" showJSON  instance Exception CompileError -showCompileError :: String -> (A.Value -> String) -> CompileError -> String+showCompileError :: String -> (Value -> String) -> CompileError -> String showCompileError format showValue = let prefix = red "\nError: "           in \case     TypeError e -> show e
tasty/Main.hs view
@@ -40,7 +40,8 @@         , testJSONToDhall "./tasty/data/emptyListStrongType"         , testJSONToDhall "./tasty/data/fromArbitraryJSON_12_0_0"         , testJSONToDhall "./tasty/data/fromArbitraryJSON_13_0_0"-        , testCustomConversionJSONToDhall omissibleLists "./tasty/data/missingList"+        , inferJSONToDhall "./tasty/data/potpourri"+        , testCustomConversionJSONToDhall False omissibleLists "./tasty/data/missingList"         , Test.Tasty.testGroup "Nesting"             [ testDhallToJSON "./tasty/data/nesting0"             , testDhallToJSON "./tasty/data/nesting1"@@ -86,11 +87,11 @@      Test.Tasty.HUnit.assertEqual message expectedValue actualValue -testCustomConversionJSONToDhall :: JSONToDhall.Conversion -> String -> TestTree-testCustomConversionJSONToDhall conv prefix =+testCustomConversionJSONToDhall+    :: Bool -> JSONToDhall.Conversion -> String -> TestTree+testCustomConversionJSONToDhall infer conv prefix =   Test.Tasty.HUnit.testCase prefix $ do     let inputFile = prefix <> ".json"-    let schemaFile = prefix <> "Schema.dhall"     let outputFile = prefix <> ".dhall"      bytes <- Data.ByteString.Lazy.readFile inputFile@@ -100,12 +101,19 @@             Left string -> fail string             Right value -> return value -    schemaText <- Data.Text.IO.readFile schemaFile+    schema <- do+        if infer+            then do+                return (JSONToDhall.schemaToDhallType (JSONToDhall.inferSchema value))+            else do+                let schemaFile = prefix <> "Schema.dhall" -    parsedSchema <- Core.throws (Dhall.Parser.exprFromText schemaFile schemaText)+                schemaText <- Data.Text.IO.readFile schemaFile -    schema <- Dhall.Import.load parsedSchema+                parsedSchema <- Core.throws (Dhall.Parser.exprFromText schemaFile schemaText) +                Dhall.Import.load parsedSchema+     _ <- Core.throws (Dhall.TypeCheck.typeOf schema)      actualExpression <- do@@ -120,7 +128,7 @@      _ <- Core.throws (Dhall.TypeCheck.typeOf resolvedExpression) -    let expectedExpression = Core.normalize resolvedExpression+    let expectedExpression = Core.denote resolvedExpression      let message =             "Conversion to Dhall did not generate the expected output"@@ -128,4 +136,9 @@     Test.Tasty.HUnit.assertEqual message expectedExpression actualExpression  testJSONToDhall :: String -> TestTree-testJSONToDhall = testCustomConversionJSONToDhall JSONToDhall.defaultConversion+testJSONToDhall =+    testCustomConversionJSONToDhall False JSONToDhall.defaultConversion++inferJSONToDhall :: String -> TestTree+inferJSONToDhall =+    testCustomConversionJSONToDhall True JSONToDhall.defaultConversion
tasty/data/emptyObject.dhall view
@@ -1,17 +1,11 @@   λ(JSON : Type) → λ ( json-    : { array :-          List JSON → JSON-      , bool :-          Bool → JSON-      , null :-          JSON-      , number :-          Double → JSON-      , object :-          List { mapKey : Text, mapValue : JSON } → JSON-      , string :-          Text → JSON+    : { array : List JSON → JSON+      , bool : Bool → JSON+      , null : JSON+      , number : Double → JSON+      , object : List { mapKey : Text, mapValue : JSON } → JSON+      , string : Text → JSON       }     )-→ json.object ([] : List { mapKey : Text, mapValue : JSON })+→ json.object (toMap {=} : List { mapKey : Text, mapValue : JSON })
tasty/data/emptyObjectStrongType.dhall view
@@ -1,1 +1,1 @@-[] : List { mapKey : Text, mapValue : Natural }+toMap {=} : List { mapKey : Text, mapValue : Natural }
tasty/data/fromArbitraryJSON_12_0_0.dhall view
@@ -9,9 +9,11 @@       }     ) → json.object-    [ { mapKey = "array", mapValue = json.array ([] : List JSON) }-    , { mapKey = "bool", mapValue = json.bool False }-    , { mapKey = "null", mapValue = json.null }-    , { mapKey = "number", mapValue = json.number 1.0 }-    , { mapKey = "string", mapValue = json.string "ABC" }-    ]+    ( toMap+        { array = json.array ([] : List JSON)+        , bool = json.bool False+        , null = json.null+        , number = json.number 1.0+        , string = json.string "ABC"+        }+    )
tasty/data/fromArbitraryJSON_13_0_0.dhall view
@@ -10,10 +10,12 @@       }     ) → json.object-    [ { mapKey = "array", mapValue = json.array ([] : List JSON) }-    , { mapKey = "bool", mapValue = json.bool False }-    , { mapKey = "double", mapValue = json.double 1.5 }-    , { mapKey = "integer", mapValue = json.integer +1 }-    , { mapKey = "null", mapValue = json.null }-    , { mapKey = "string", mapValue = json.string "ABC" }-    ]+    ( toMap+        { array = json.array ([] : List JSON)+        , bool = json.bool False+        , double = json.double 1.5+        , integer = json.integer +1+        , null = json.null+        , string = json.string "ABC"+        }+    )
tasty/data/missingList.dhall view
@@ -1,1 +1,4 @@-{present = ["some-stuff"], null = [] : List Text, missing = [] : List Text}+{ missing = [] : List Text+, null = [] : List Text+, present = ["some-stuff"]+}
+ tasty/data/potpourri.dhall view
@@ -0,0 +1,53 @@+[ { arbitraryJSON =+      λ(JSON : Type) →+      λ ( json+        : { array : List JSON → JSON+          , bool : Bool → JSON+          , double : Double → JSON+          , integer : Integer → JSON+          , null : JSON+          , object : List { mapKey : Text, mapValue : JSON } → JSON+          , string : Text → JSON+          }+        ) →+        json.integer +1+  , bool = True+  , double = 0.0+  , extraField = None Bool+  , integer = +0+  , natural = 0+  , null = None <>+  , optional = Some 1+  , preferDouble = 0.0+  , preferInteger = +0+  , text = "abc"+  , unifyEmpty = [] : List Natural+  , unifySimple = < Bool : Bool | Text : Text >.Bool True+  }+, { arbitraryJSON =+      λ(JSON : Type) →+      λ ( json+        : { array : List JSON → JSON+          , bool : Bool → JSON+          , double : Double → JSON+          , integer : Integer → JSON+          , null : JSON+          , object : List { mapKey : Text, mapValue : JSON } → JSON+          , string : Text → JSON+          }+        ) →+        json.array ([] : List JSON)+  , bool = False+  , double = 1.1+  , extraField = Some True+  , integer = -1+  , natural = 1+  , null = None <>+  , optional = None Natural+  , preferDouble = 1.1+  , preferInteger = -1+  , text = "def"+  , unifyEmpty = [ 1 ]+  , unifySimple = < Bool : Bool | Text : Text >.Text "abc"+  }+]
+ tasty/data/potpourri.json view
@@ -0,0 +1,27 @@+[ { "bool": true+  , "text": "abc"+  , "natural": 0+  , "integer": 0+  , "double": 0.0+  , "unifyEmpty": []+  , "preferInteger": 0+  , "preferDouble": 0+  , "unifySimple": true+  , "optional": 1+  , "null": null+  , "arbitraryJSON": 1+  }+, { "bool": false+  , "text": "def"+  , "natural": 1+  , "integer": -1+  , "double": 1.1+  , "unifyEmpty": [1]+  , "preferInteger": -1+  , "preferDouble": 1.1+  , "unifySimple": "abc"+  , "optional": null+  , "extraField": true+  , "arbitraryJSON": []+  }+]