safe-json-0.1.0: Automatic JSON format versioning

Copyright(c) 2019 Felix Paulusma
LicenseMIT
Maintainerfelix.paulusma@gmail.com
Stabilityexperimental
Safe HaskellNone
LanguageHaskell2010

Data.SafeJSON

Contents

Description

Please read the

README on GitHub

for an extensive explanation of this library, why and how to use it, and examples.

Synopsis

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 Object, an extra field with the version number will be added.

Example value:
  {"type":"test", "data":true}

Resulting object:
  {"!v": 1, "type":"test", "data":true}

If the resulting Value is not an Object, it will be wrapped in one, with a version field:

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 (ToJSON a, FromJSON a) => 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.

Methods

version :: Version a Source #

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).

kind :: Kind a Source #

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.

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.

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

data Contained a Source #

This is an inpenetrable container. A security measure used to ensure safeFrom and safeTo are never used directly. Instead, always use safeFromJSON and safeToJSON.

contain :: a -> Contained a Source #

Used when defining safeFrom or safeTo.

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.

data Version a Source #

A simple numeric version id.

Version has a Num instance and should be declared using integer literals: version = 2

Instances

Eq (Version a) Source # 

Methods

(==) :: Version a -> Version a -> Bool #

(/=) :: Version a -> Version a -> Bool #

Num (Version a) Source #

It is strongly discouraged to use any methods other than fromInteger of Version 's Num instance.

Methods

(+) :: Version a -> Version a -> Version a #

(-) :: Version a -> Version a -> Version a #

(*) :: Version a -> Version a -> Version a #

negate :: Version a -> Version a #

abs :: Version a -> Version a #

signum :: Version a -> Version a #

fromInteger :: Integer -> Version a #

Show (Version a) Source # 

Methods

showsPrec :: Int -> Version a -> ShowS #

show :: Version a -> String #

showList :: [Version a] -> ShowS #

Arbitrary (Version a) Source #

This instance explicitly doesn't consider noVersion, since it is an exception in almost every sense.

Methods

arbitrary :: Gen (Version a) #

shrink :: Version a -> [Version a] #

noVersion :: Version a Source #

This is used for types that don't have a version tag.

This is used for primitive values that are 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. version = noVersion is distinctively different from version = 0, which will add a version tag with the number 0 (zero), whereas noVersion will not add a version tag.

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: version = noVersion. N.B. base and extended_base are the only kinds that can be paired with noVersion.
  • Extensions (see extension and extended_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 Migrate a instance)
  • Forward extensions (see extended_base and extended_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 Migrate (Reverse a) instance)

data Kind a Source #

The kind of a SafeJSON type determines how it can be migrated to.

base :: Kind a Source #

Used to define kind. Base types do not extend any type.

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

data Profile a Source #

Profile of the internal consistency of a SafeJSON instance.

N.B. noVersion shows as null instead of a number.

Constructors

InvalidProfile String

There is something wrong with versioning

Profile ProfileVersions

Profile of consistent versions

Instances

Eq (Profile a) Source # 

Methods

(==) :: Profile a -> Profile a -> Bool #

(/=) :: Profile a -> Profile a -> Bool #

Typeable * a => Show (Profile a) Source # 

Methods

showsPrec :: Int -> Profile a -> ShowS #

show :: Profile a -> String #

showList :: [Profile a] -> ShowS #

data ProfileVersions Source #

Version profile of a consistent SafeJSON instance.

Constructors

ProfileVersions 

Fields

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 (Migrate a) migrates from the previous version to the type a, (Migrate (Reverse a)) migrates from the future version to the type a.

Example

Two types that can migrate to each other.

(Don't forget to give OldType one of the extended kinds, and NewType one of the extension kinds.)

instance Migrate NewType where
  type MigrateFrom NewType = OldType
  migrate OldType = NewType

instance Migrate (Reverse OldType) where
  type MigrateFrom (Reverse OldType) = NewType
  migrate NewType = Reverse OldType

Minimal complete definition

migrate

Associated Types

type MigrateFrom a Source #

The type from which will be migrated to type a

Methods

migrate :: MigrateFrom a -> a Source #

The migration from the previous version to the current type a. OR, in case of a (Reverse a), the migration from the future version back to the current type a

newtype Reverse a Source #

This is a wrapper type used migrating backwards in the chain of compatible types.

This is useful when running updates in production where new-format JSON will be received by old-format expecting programs.

Constructors

Reverse 

Fields