yamlet-aeson
Safe HaskellNone
LanguageGHC2021

Yamlet.Aeson

Description

Use the FromJSON and ToJSON instances of aeson with yamlet, for a type that has no FromYaml and ToYaml instances yet, e.g. in a program that moves from the yaml package to yamlet.

The examples use these external imports:

>>> import Data.Aeson qualified as A
>>> import Data.Map.Strict qualified as M
>>> import Data.Text qualified as T
>>> import Data.Text.IO qualified as T

A value in ViaAeson decodes and encodes with the FromJSON and ToJSON instances:

>>> :{
data Server = Server {port :: Int, host :: T.Text}
  deriving stock (Generic, Show)
  deriving anyclass (A.FromJSON)
instance A.ToJSON Server where
  toEncoding = A.genericToEncoding A.defaultOptions
:}
>>> Right (ViaAeson server) = decodeText @(ViaAeson Server) "port: 80\nhost: localhost\n"
>>> server
Server {port = 80, host = "localhost"}
>>> T.putStr (encodeText (ViaAeson server))
port: 80
host: localhost

A type with FromJSON and ToJSON instances can derive its FromYaml and ToYaml instances via ViaAeson, e.g. in a program that reads and writes both JSON and YAML and keeps one set of instances. Such a type can be a field of a type with instances of its own:

>>> :{
newtype Address = Address T.Text
  deriving stock (Show)
  deriving newtype (A.FromJSON, A.ToJSON)
  deriving (FromYaml, ToYaml) via ViaAeson Address
data Mail = Mail {from :: Address, to :: [Address]}
  deriving stock (Generic, Show)
  deriving anyclass (GenericYamlOptions)
  deriving (FromYaml, ToYaml) via GenericYaml Mail
:}
>>> decodeText @Mail "from: a@example.com\nto: [b@example.com]\n"
Right (Mail {from = Address "a@example.com", to = [Address "b@example.com"]})

An error of a decoder of aeson points to the node that caused it:

>>> input = "- port: 80\n  host: a\n- port: http\n  host: b\n"
>>> T.putStr input
- port: 80
  host: a
- port: http
  host: b
>>> either printErrors print (decodeText @(ViaAeson [Server]) input)
input.yaml:3:9: [1].port: parsing Int failed, expected Number, but encountered String
  |
3 | - port: http
  |         ^

The module also has the FromYaml and ToYaml instances for an aeson Value, e.g. for a field that holds any data. They convert as the section Conversion says:

>>> decodeText @A.Value "name: a\nports: [80, 443]\n"
Right (Object (fromList [("name",String "a"),("ports",Array [Number 80.0,Number 443.0])]))

Order of keys

The encoder writes the keys of a mapping in the order of toEncoding. That is the order of the fields if the instance defines toEncoding, e.g. with genericToEncoding as Server above, or with deriveJSON. The default toEncoding goes through toJSON, so the keys come in the order of an aeson object, which is sorted by default.

parseJSON gets the keys in no order, because an aeson object has none. To keep the order of a mapping, decode the mapping with a decoder of yamlet. Its values can still decode via ViaAeson:

>>> :{
newtype Servers = Servers [(T.Text, Server)]
  deriving stock (Show)
instance FromYaml Servers where
  parseYaml = withMapping $ \o ->
    Servers
      <$> traverse
        ( \(k, v) ->
            (,) <$> parseYaml k <*> fmap (.value) (parseYaml @(ViaAeson Server) v)
        )
        (objectEntries o)
:}
>>> decodeText @Servers "web: {port: 80, host: a}\napi: {port: 81, host: b}\n"
Right (Servers [("web",Server {port = 80, host = "a"}),("api",Server {port = 81, host = "b"})])

decodeWithDocument keeps the whole document with the decoded value, e.g. to write the document back with a change.

Conversion

A YAML document converts to an aeson Value as follows:

  • A key is the text of its scalar, e.g. "0x10" for 0x10 and "~" for ~, as in the yaml package. Two keys with the same text are an error, e.g. 1 and "1", which are different keys in YAML. A key that is a collection is an error.
  • A key << is an ordinary key, because the merge keys of YAML 1.1 are not supported.
  • .inf and -.inf are the strings "+inf" and "-inf", and .nan is null. The FromJSON and ToJSON instances for Double and Float read and write these values.
  • -0.0 is the number 0, because a Scientific has no negative zero.
  • A number whose exponent in scientific notation is beyond the range from -1000 to 1000, e.g. 1e1001, is an error, as in yamlet. A Number converts to YAML as aeson writes it in JSON: as an integer if its base10Exponent is from 0 to 1024, otherwise in scientific notation. Thus 1e1001 reads back as an integer, but 1e1025 and 1e-1001 do not read back.
  • A tag that is not of the core schema makes a scalar a string, e.g. !secret 123 is the string "123". The yaml package reads it as the number 123. A collection with such a tag converts as without it, e.g. !point {x: 1} is the object {"x": 1}.

A FromJSON instance can convert two different keys to the same key and then keep only one of the pairs, as it does for JSON, e.g. 1 and 1.0 for a Map Int. The FromYaml instances for maps reject such keys.

>>> decodeText @(ViaAeson (M.Map Int T.Text)) "1: a\n1.0: b\n"
Right (ViaAeson {value = fromList [(1,"a")]})

An aeson Value converts to YAML as aeson writes it in JSON, e.g. the keys of a Map Int are strings, which the encoder quotes because they look like numbers:

>>> T.putStr (encodeText (ViaAeson (M.fromList @Int @T.Text [(1, "a")])))
'1': a
Synopsis

Documentation

newtype ViaAeson a Source #

A value that decodes and encodes with its FromJSON and ToJSON instances. The field has no selector function, so read it with record dot syntax, e.g. (.value), or with a pattern.

Constructors

ViaAeson 

Fields

Instances

Instances details
Show a => Show (ViaAeson a) Source # 
Instance details

Defined in Yamlet.Aeson

Methods

showsPrec :: Int -> ViaAeson a -> ShowS

show :: ViaAeson a -> String

showList :: [ViaAeson a] -> ShowS

Eq a => Eq (ViaAeson a) Source # 
Instance details

Defined in Yamlet.Aeson

Methods

(==) :: ViaAeson a -> ViaAeson a -> Bool

(/=) :: ViaAeson a -> ViaAeson a -> Bool

Ord a => Ord (ViaAeson a) Source # 
Instance details

Defined in Yamlet.Aeson

Methods

compare :: ViaAeson a -> ViaAeson a -> Ordering

(<) :: ViaAeson a -> ViaAeson a -> Bool

(<=) :: ViaAeson a -> ViaAeson a -> Bool

(>) :: ViaAeson a -> ViaAeson a -> Bool

(>=) :: ViaAeson a -> ViaAeson a -> Bool

max :: ViaAeson a -> ViaAeson a -> ViaAeson a

min :: ViaAeson a -> ViaAeson a -> ViaAeson a

FromJSON a => FromYaml (ViaAeson a) Source #

The value with the FromJSON instance. An error of parseJSON points to the node at its path. If the node has no such path, the error points to the deepest node of the path and names the rest of it.

An error of a key of a map points to the value of the key, because aeson gives it the same path as an error of the value. The path at the start of the message names the key:

>>> either printErrors print (decodeText @(ViaAeson (M.Map Int T.Text)) "1: a\nabc: b\n")
input.yaml:2:6: abc: parsing Int failed, Unexpected 'a' while parsing number literal
  |
2 | abc: b
  |      ^
Instance details

Defined in Yamlet.Aeson

Methods

parseYaml :: Node -> Parser (ViaAeson a)

parseYamlList :: Node -> Parser [ViaAeson a]

parseYamlField :: Node -> Node -> Parser (ViaAeson a)

ToJSON a => ToYaml (ViaAeson a) Source #

The value with the ToJSON instance, with the keys of each mapping in the order of toEncoding. Of two equal keys, the first one stays, as when aeson decodes JSON.

If the encoding is not valid JSON, which only unsafeToEncoding can cause, the conversion throws an error.

Instance details

Defined in Yamlet.Aeson

Methods

toYaml :: ViaAeson a -> Node

toYamlList :: [ViaAeson a] -> Node

toYamlField :: Node -> ViaAeson a -> (Node, Node)

Orphan instances

FromYaml Value Source #

The value of a node, converted as the section Conversion says.

Instance details

Methods

parseYaml :: Node -> Parser Value

parseYamlList :: Node -> Parser [Value]

parseYamlField :: Node -> Node -> Parser Value

ToYaml Value Source #

The value as aeson writes it in JSON.

Instance details

Methods

toYaml :: Value -> Node

toYamlList :: [Value] -> Node

toYamlField :: Node -> Value -> (Node, Node)