Copyright | (c) 2019 Felix Paulusma |
---|---|
License | MIT |
Maintainer | felix.paulusma@gmail.com |
Stability | experimental |
Safe Haskell | None |
Language | Haskell2010 |
Please read the
for an extensive explanation of this library, why and how to use it, and examples.
- safeToJSON :: forall a. SafeJSON a => a -> Value
- safeFromJSON :: forall a. SafeJSON a => Value -> Parser a
- class SafeJSON a where
- data Contained a
- contain :: a -> Contained a
- containWithObject :: String -> (Object -> Parser a) -> Value -> Contained (Parser a)
- containWithArray :: String -> (Array -> Parser a) -> Value -> Contained (Parser a)
- containWithText :: String -> (Text -> Parser a) -> Value -> Contained (Parser a)
- containWithScientific :: String -> (Scientific -> Parser a) -> Value -> Contained (Parser a)
- containWithBool :: String -> (Bool -> Parser a) -> Value -> Contained (Parser a)
- (.:$) :: SafeJSON a => Object -> Text -> Parser a
- (.:$?) :: SafeJSON a => Object -> Text -> Parser (Maybe a)
- (.:$!) :: SafeJSON a => Object -> Text -> Parser (Maybe a)
- (.=$) :: (SafeJSON a, KeyValue kv) => Text -> a -> kv
- data Version a
- noVersion :: Version a
- setVersion :: forall a. SafeJSON a => Value -> Value
- setVersion' :: forall a. SafeJSON a => Version a -> Value -> Value
- removeVersion :: Value -> Value
- data Kind a
- base :: Kind a
- extension :: (SafeJSON a, Migrate a) => Kind a
- extended_base :: (SafeJSON a, Migrate (Reverse a)) => Kind a
- extended_extension :: (SafeJSON a, Migrate a, Migrate (Reverse a)) => Kind a
- typeName0 :: Typeable a => Proxy a -> String
- typeName1 :: forall t a. Typeable t => Proxy (t a) -> String
- typeName2 :: forall t a b. Typeable t => Proxy (t a b) -> String
- typeName3 :: forall t a b c. Typeable t => Proxy (t a b c) -> String
- typeName4 :: forall t a b c d. Typeable t => Proxy (t a b c d) -> String
- typeName5 :: forall t a b c d e. Typeable t => Proxy (t a b c d e) -> String
- data Profile a
- data ProfileVersions = ProfileVersions {
- profileCurrentVersion :: Maybe Int32
- profileSupportedVersions :: [(Maybe Int32, String)]
- class SafeJSON (MigrateFrom a) => Migrate a where
- type MigrateFrom a
- newtype Reverse a = Reverse {
- unReverse :: a
Conversion to/from versioned JSON
These functions are the workhorses of the library.
As long as a type has a SafeJSON
instance and, if conversion
from other types is required, a Migrate
instance, these will
make sure to add and read version numbers, and handle migration.
safeToJSON :: forall a. SafeJSON a => a -> Value Source #
Use this exactly how you would use toJSON
from Data.Aeson.
Though most use cases will probably use one of the encode
functions from Data.Aeson.Safe.
safeToJSON
will add a version tag to the Value
created.
If the Value
resulting from safeTo
(by default the same as toJSON
)
is an
, an extra field with the version number will be added.Object
Example value: {"type":"test", "data":true} Resulting object: {"!v": 1, "type":"test", "data":true}
If the resulting Value
is not an
, it will be wrapped
in one, with a version field:Object
Example value: "arbitrary string" Resulting object: {"~v": 1, "~d": "arbitrary string"}
This function does not check consistency of the SafeJSON
instances.
It is advised to always testConsistency
for all
your instances in a production setting.
safeFromJSON :: forall a. SafeJSON a => Value -> Parser a Source #
Use this exactly how you would use parseJSON
from Data.Aeson.
Though most use cases will probably use one of the decode
functions from Data.Aeson.Safe.
safeFromJSON
tries to find the version number in the JSON
Value
provided, find the appropriate parser and migrate the
parsed result back to the requested type using Migrate
instances.
If there is no version number (that means this can also happen with
completely unrelated JSON messages), and there is a SafeJSON
instance in the chain that has version
defined as noVersion
,
it will try to parse that type.
N.B. If the consistency of the SafeJSON
instance in
question is faulty, this will always fail.
SafeJSON Class
This class, together with Migrate
, is where the magic happens!
Using the SafeJSON
class to define the form and expected
migration to a type, and defining Migrate
instances to describe
how to handle the conversion from older versions (or maybe a
newer version) to the type, you can be sure that your programs
will still parse the JSON of types it is expecting.
class SafeJSON a where Source #
A type that can be converted from and to JSON with versioning baked
in, using Migrate
to automate migration between versions, reducing
headaches when the need arrises to modify JSON formats while old
formats can't simply be disregarded.
The version of the type.
Only used as a key so it must be unique (this is checked at run-time)
Version numbering doesn't have to be sequential or continuous.
The default version is 0 (zero).
The kind specifies how versions are dealt with. By default, values are tagged with version 0 and don't have any previous versions.
The default kind is base
safeTo :: a -> Contained Value Source #
This method defines how a value should be serialized without worrying
about adding the version. The default implementation uses toJSON
, but
can be modified if need be.
This function cannot be used directly. Use safeToJSON
, instead.
safeTo :: ToJSON a => a -> Contained Value Source #
This method defines how a value should be serialized without worrying
about adding the version. The default implementation uses toJSON
, but
can be modified if need be.
This function cannot be used directly. Use safeToJSON
, instead.
safeFrom :: Value -> Contained (Parser a) Source #
This method defines how a value should be parsed without also worrying
about writing out the version tag. The default implementation uses parseJSON
,
but can be modified if need be.
This function cannot be used directly. Use safeFromJSON
, instead.
safeFrom :: FromJSON a => Value -> Contained (Parser a) Source #
This method defines how a value should be parsed without also worrying
about writing out the version tag. The default implementation uses parseJSON
,
but can be modified if need be.
This function cannot be used directly. Use safeFromJSON
, instead.
typeName :: Proxy a -> String Source #
The name of the type. This is used in error message strings and the
Profile
report.
Doesn't have to be defined if your type is Typeable
. The default
implementation is typeName0
. (cf. typeName1
, typeName2
, etc.)
typeName :: Typeable a => Proxy a -> String Source #
The name of the type. This is used in error message strings and the
Profile
report.
Doesn't have to be defined if your type is Typeable
. The default
implementation is typeName0
. (cf. typeName1
, typeName2
, etc.)
objectProfile :: Profile a Source #
Version profile.
Shows the current version of the type and all supported versions it can migrate from.
Contained
This is an impenetrable container. A security measure
used to ensure safeFrom
and safeTo
are never used
directly. Instead, always use safeFromJSON
and
safeToJSON
.
Defining safeFrom
and safeTo
If the type doesn't already have FromJSON
and
ToJSON
instances, the following functions can help
in defining the safeFrom
and safeTo
methods.
safeFrom = containWithObject "MyType" $ \o -> MyType <$> o .: "regular_value" <*> o .:$ "safe_value" safeTo (MyType regular safe) = contain . object $ [ "regular_value" .= regular , "safe_value" .=$ safe ]
Inspecting values in safeFrom
The following functions are helpful when defining safeFrom
.
They are basically contain
composed with the corresponding
Data.Aeson function, so they can be used in the same fashion
as said Data.Aeson function.
containWithObject :: String -> (Object -> Parser a) -> Value -> Contained (Parser a) Source #
Similar to withObject
, but contain
ed to be used
in safeFrom
definitions
Since: 1.0.0
containWithScientific :: String -> (Scientific -> Parser a) -> Value -> Contained (Parser a) Source #
Similar to withScientific
, but contain
ed to be used
in safeFrom
definitions
Since: 1.0.0
Accessors
These accessors can be used like their Data.Aeson counterparts.
The only difference is that the expected value is parsed using
safeFromJSON
instead of parseJSON
.
(.:$) :: SafeJSON a => Object -> Text -> Parser a Source #
Similar to .:
, but uses safeFromJSON
instead of parseJSON
to parse the value in the given field.
Since: 1.0.0
(.:$?) :: SafeJSON a => Object -> Text -> Parser (Maybe a) Source #
Similar to .:?
, but uses safeFromJSON
instead of parseJSON
to maybe parse the value in the given field.
Since: 1.0.0
(.:$!) :: SafeJSON a => Object -> Text -> Parser (Maybe a) Source #
Similar to .:!
, but uses safeFromJSON
instead of parseJSON
to maybe parse the value in the given field.
Since: 1.0.0
Constructor for safeTo
This constructor of key-value pairs can be used exactly like
its Data.Aeson counterpart (.=
), but converts the
given value with safeToJSON
instead of toJSON
(.=$) :: (SafeJSON a, KeyValue kv) => Text -> a -> kv Source #
Similarly to .=
, but uses safeToJSON
instead of toJSON
to convert the value in that key-value pair.
Since: 1.0.0
Version
All SafeJSON
instances have a version
. This version will be
attached to the JSON format and used to figure out which parser
(and as such, which type in the chain) should be used to parse
the given JSON.
A simple numeric version id.
Version
has a Num
instance and should be
declared using integer literals: version
= 2
Eq (Version a) Source # | |
Num (Version a) Source # | It is strongly discouraged to use any methods other
than |
Show (Version a) Source # | |
Arbitrary (Version a) Source # | This instance explicitly doesn't consider |
noVersion :: Version a Source #
This is used for types that don't have a version tag.
This is used for primitive values that are not tagged with
a version number, like Int
, Text
, [a]
, etc.
But also when implementing SafeJSON
after the fact,
when a format is already in use, but you still want to
be able to migrate
from it to a newer type or format.
N.B.
is distinctively different
from version
= noVersion
, which will add a version tag with
the number 0 (zero), whereas version
= 0noVersion
will not add a
version
tag.
setVersion :: forall a. SafeJSON a => Value -> Value Source #
CAUTION: Only use this function if you know what you're doing.
The version will be set top-level, without inspection of the Value
!
(cf. removeVersion
) In some rare cases, you might want to interpret
a versionless Value
as a certain type/version. setVersion
allows
you to (unsafely) insert a version field.
If possible, it is advised to use a FromJSON
instance instead.
(One that doesn't also use safeFromJSON
in its methods!)
This might be needed when data sent to an API endpoint doesn't need to implement SafeJSON standards. E.g. in the case of endpoints for third parties or customers.
USAGE: {-# LANGUAGE TypeApplications #-} data Test = Test String instanceSafeJSON
Test where ... >>> val =String
"test" ::Value
String "test" >>>encode
val ""test"" >>>encode
$setVersion
@Test val "{"~v":0,"~d":"test"}" >>> parseMaybesafeFromJSON
$setVersion
@Test val Just (Test "test")
Since: 1.0.0
setVersion' :: forall a. SafeJSON a => Version a -> Value -> Value Source #
Same as setVersion
, but requires a Version
parameter.
>>>
'encode' $ 'setVersion'' ('version' :: 'Version' Test) val
"{\"~v\":0,\"~d\":\"test\"}"
Since: 1.0.0
removeVersion :: Value -> Value Source #
CAUTION: Only use this function if you know what you're doing.
(cf. setVersion
) removeVersion
removes all the SafeJSON
versioning from a JSON Value
. Even recursively.
This might be necessary if the resulting JSON is sent to a
third party (e.g. customer) and the SafeJSON
versioning
should be hidden.
Since: 1.0.0
Kind
All SafeJSON
instance have a declared kind
, indicating if any
migration needs to happen when parsing using safeFromJSON
.
- The Base kind (see
base
) is at the bottom of the chain and will not be migrated to. They can optionally have no version tag by defining:
. N.B.version
=noVersion
base
andextended_base
are the only kinds that can be paired withnoVersion
. - Extensions (see
extension
andextended_extension
) tell the system that there exists at least one previous version of the data type which should be migrated from if needed. (This requires the data type to also have a
instance)Migrate
a - Forward extensions (see
extended_base
andextended_extension
) tell the system there exists at least one next version from which the data type can be reverse-migrated. (This requires the data type to also have a
instance)Migrate
(Reverse
a)
extension :: (SafeJSON a, Migrate a) => Kind a Source #
Used to define kind
.
Extends a previous version.
extended_base :: (SafeJSON a, Migrate (Reverse a)) => Kind a Source #
Used to define kind
.
Types that are extended_base
, are extended by a
future version and as such can migrate backward from
that future version. (cf. extended_extension
, base
)
extended_extension :: (SafeJSON a, Migrate a, Migrate (Reverse a)) => Kind a Source #
Used to define kind
.
Types that are extended_extension
are extended
by a future version and as such can migrate from
that future version, but they also extend a previous
version. (cf. extended_base
, extension
)
Showing the type
These helper functions can be used to easily define typeName
.
As long as the type being defined has a Typeable
instance.
typeName0 :: Typeable a => Proxy a -> String Source #
Type name string representation of a nullary type constructor.
typeName1 :: forall t a. Typeable t => Proxy (t a) -> String Source #
Type name string representation of a unary type constructor.
typeName2 :: forall t a b. Typeable t => Proxy (t a b) -> String Source #
Type name string representation of a binary type constructor.
typeName3 :: forall t a b c. Typeable t => Proxy (t a b c) -> String Source #
Type name string representation of a ternary type constructor.
typeName4 :: forall t a b c d. Typeable t => Proxy (t a b c d) -> String Source #
Type name string representation of a 4-ary type constructor.
typeName5 :: forall t a b c d e. Typeable t => Proxy (t a b c d e) -> String Source #
Type name string representation of a 5-ary type constructor.
Consistency
Profile of the internal consistency of a SafeJSON
instance.
N.B. noVersion
shows as null
instead of a number.
InvalidProfile String | There is something wrong with versioning |
Profile ProfileVersions | Profile of consistent versions |
data ProfileVersions Source #
Version profile of a consistent SafeJSON
instance.
ProfileVersions | |
|
Eq ProfileVersions Source # | |
Show ProfileVersions Source # |
|
Migration
class SafeJSON (MigrateFrom a) => Migrate a where Source #
This instance is needed to handle the migration between older and newer versions.
Note that, where (
migrates from the previous
version to the type Migrate
a)a
, (
migrates
from the future version to the type Migrate
(Reverse
a))a
.
Example
Two types that can migrate to each other.
(Don't forget to give OldType
one of the extended
kind
s,
and NewType
one of the extension
kind
s.)
instanceMigrate
NewType where typeMigrateFrom
NewType = OldTypemigrate
OldType = NewType instanceMigrate
(Reverse
OldType) where typeMigrateFrom
(Reverse
OldType) = NewTypemigrate
NewType =Reverse
OldType
type MigrateFrom a Source #
The type from which will be migrated to type a
migrate :: MigrateFrom a -> a Source #
The migration from the previous version to the
current type a
. OR, in case of a (
,
the migration from the future version back to
the current type Reverse
a)a