versioning (empty) → 0.1.0.0
raw patch · 8 files changed
+676/−0 lines, 8 filesdep +aesondep +basedep +bytestringsetup-changed
Dependencies added: aeson, base, bytestring, hspec, versioning
Files
- ChangeLog.md +5/−0
- LICENSE +202/−0
- Setup.hs +2/−0
- src/Versioning/Base.hs +156/−0
- src/Versioning/JSON.hs +103/−0
- src/Versioning/Upgrade.hs +57/−0
- tests/Main.hs +113/−0
- versioning.cabal +38/−0
+ ChangeLog.md view
@@ -0,0 +1,5 @@+# Revision history for version++## 0.1.0.0 -- 2018-06-06++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,202 @@++ Apache License+ Version 2.0, January 2004+ http://www.apache.org/licenses/++ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++ 1. Definitions.++ "License" shall mean the terms and conditions for use, reproduction,+ and distribution as defined by Sections 1 through 9 of this document.++ "Licensor" shall mean the copyright owner or entity authorized by+ the copyright owner that is granting the License.++ "Legal Entity" shall mean the union of the acting entity and all+ other entities that control, are controlled by, or are under common+ control with that entity. For the purposes of this definition,+ "control" means (i) the power, direct or indirect, to cause the+ direction or management of such entity, whether by contract or+ otherwise, or (ii) ownership of fifty percent (50%) or more of the+ outstanding shares, or (iii) beneficial ownership of such entity.++ "You" (or "Your") shall mean an individual or Legal Entity+ exercising permissions granted by this License.++ "Source" form shall mean the preferred form for making modifications,+ including but not limited to software source code, documentation+ source, and configuration files.++ "Object" form shall mean any form resulting from mechanical+ transformation or translation of a Source form, including but+ not limited to compiled object code, generated documentation,+ and conversions to other media types.++ "Work" shall mean the work of authorship, whether in Source or+ Object form, made available under the License, as indicated by a+ copyright notice that is included in or attached to the work+ (an example is provided in the Appendix below).++ "Derivative Works" shall mean any work, whether in Source or Object+ form, that is based on (or derived from) the Work and for which the+ editorial revisions, annotations, elaborations, or other modifications+ represent, as a whole, an original work of authorship. For the purposes+ of this License, Derivative Works shall not include works that remain+ separable from, or merely link (or bind by name) to the interfaces of,+ the Work and Derivative Works thereof.++ "Contribution" shall mean any work of authorship, including+ the original version of the Work and any modifications or additions+ to that Work or Derivative Works thereof, that is intentionally+ submitted to Licensor for inclusion in the Work by the copyright owner+ or by an individual or Legal Entity authorized to submit on behalf of+ the copyright owner. For the purposes of this definition, "submitted"+ means any form of electronic, verbal, or written communication sent+ to the Licensor or its representatives, including but not limited to+ communication on electronic mailing lists, source code control systems,+ and issue tracking systems that are managed by, or on behalf of, the+ Licensor for the purpose of discussing and improving the Work, but+ excluding communication that is conspicuously marked or otherwise+ designated in writing by the copyright owner as "Not a Contribution."++ "Contributor" shall mean Licensor and any individual or Legal Entity+ on behalf of whom a Contribution has been received by Licensor and+ subsequently incorporated within the Work.++ 2. Grant of Copyright License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ copyright license to reproduce, prepare Derivative Works of,+ publicly display, publicly perform, sublicense, and distribute the+ Work and such Derivative Works in Source or Object form.++ 3. Grant of Patent License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ (except as stated in this section) patent license to make, have made,+ use, offer to sell, sell, import, and otherwise transfer the Work,+ where such license applies only to those patent claims licensable+ by such Contributor that are necessarily infringed by their+ Contribution(s) alone or by combination of their Contribution(s)+ with the Work to which such Contribution(s) was submitted. If You+ institute patent litigation against any entity (including a+ cross-claim or counterclaim in a lawsuit) alleging that the Work+ or a Contribution incorporated within the Work constitutes direct+ or contributory patent infringement, then any patent licenses+ granted to You under this License for that Work shall terminate+ as of the date such litigation is filed.++ 4. Redistribution. You may reproduce and distribute copies of the+ Work or Derivative Works thereof in any medium, with or without+ modifications, and in Source or Object form, provided that You+ meet the following conditions:++ (a) You must give any other recipients of the Work or+ Derivative Works a copy of this License; and++ (b) You must cause any modified files to carry prominent notices+ stating that You changed the files; and++ (c) You must retain, in the Source form of any Derivative Works+ that You distribute, all copyright, patent, trademark, and+ attribution notices from the Source form of the Work,+ excluding those notices that do not pertain to any part of+ the Derivative Works; and++ (d) If the Work includes a "NOTICE" text file as part of its+ distribution, then any Derivative Works that You distribute must+ include a readable copy of the attribution notices contained+ within such NOTICE file, excluding those notices that do not+ pertain to any part of the Derivative Works, in at least one+ of the following places: within a NOTICE text file distributed+ as part of the Derivative Works; within the Source form or+ documentation, if provided along with the Derivative Works; or,+ within a display generated by the Derivative Works, if and+ wherever such third-party notices normally appear. The contents+ of the NOTICE file are for informational purposes only and+ do not modify the License. You may add Your own attribution+ notices within Derivative Works that You distribute, alongside+ or as an addendum to the NOTICE text from the Work, provided+ that such additional attribution notices cannot be construed+ as modifying the License.++ You may add Your own copyright statement to Your modifications and+ may provide additional or different license terms and conditions+ for use, reproduction, or distribution of Your modifications, or+ for any such Derivative Works as a whole, provided Your use,+ reproduction, and distribution of the Work otherwise complies with+ the conditions stated in this License.++ 5. Submission of Contributions. Unless You explicitly state otherwise,+ any Contribution intentionally submitted for inclusion in the Work+ by You to the Licensor shall be under the terms and conditions of+ this License, without any additional terms or conditions.+ Notwithstanding the above, nothing herein shall supersede or modify+ the terms of any separate license agreement you may have executed+ with Licensor regarding such Contributions.++ 6. Trademarks. This License does not grant permission to use the trade+ names, trademarks, service marks, or product names of the Licensor,+ except as required for reasonable and customary use in describing the+ origin of the Work and reproducing the content of the NOTICE file.++ 7. Disclaimer of Warranty. Unless required by applicable law or+ agreed to in writing, Licensor provides the Work (and each+ Contributor provides its Contributions) on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+ implied, including, without limitation, any warranties or conditions+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+ PARTICULAR PURPOSE. You are solely responsible for determining the+ appropriateness of using or redistributing the Work and assume any+ risks associated with Your exercise of permissions under this License.++ 8. Limitation of Liability. In no event and under no legal theory,+ whether in tort (including negligence), contract, or otherwise,+ unless required by applicable law (such as deliberate and grossly+ negligent acts) or agreed to in writing, shall any Contributor be+ liable to You for damages, including any direct, indirect, special,+ incidental, or consequential damages of any character arising as a+ result of this License or out of the use or inability to use the+ Work (including but not limited to damages for loss of goodwill,+ work stoppage, computer failure or malfunction, or any and all+ other commercial damages or losses), even if such Contributor+ has been advised of the possibility of such damages.++ 9. Accepting Warranty or Additional Liability. While redistributing+ the Work or Derivative Works thereof, You may choose to offer,+ and charge a fee for, acceptance of support, warranty, indemnity,+ or other liability obligations and/or rights consistent with this+ License. However, in accepting such obligations, You may act only+ on Your own behalf and on Your sole responsibility, not on behalf+ of any other Contributor, and only if You agree to indemnify,+ defend, and hold each Contributor harmless for any liability+ incurred by, or claims asserted against, such Contributor by reason+ of your accepting any such warranty or additional liability.++ END OF TERMS AND CONDITIONS++ APPENDIX: How to apply the Apache License to your work.++ To apply the Apache License to your work, attach the following+ boilerplate notice, with the fields enclosed by brackets "[]"+ replaced with your own identifying information. (Don't include+ the brackets!) The text should be enclosed in the appropriate+ comment syntax for the file format. We also recommend that a+ file or class name and description of purpose be included on the+ same "printed page" as the copyright notice for easier+ identification within third-party archives.++ Copyright [yyyy] [name of copyright owner]++ Licensed under the Apache License, Version 2.0 (the "License");+ you may not use this file except in compliance with the License.+ You may obtain a copy of the License at++ http://www.apache.org/licenses/LICENSE-2.0++ Unless required by applicable law or agreed to in writing, software+ distributed under the License is distributed on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ See the License for the specific language governing permissions and+ limitations under the License.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Versioning/Base.hs view
@@ -0,0 +1,156 @@+-- | This module provides some tools to encode multiple versions+-- of a data model in a single data-type parametrized by version number.+-- The addition or removal of a field can be expressed through the+-- 'Since' and 'Until' type families.+--+-- Example:+-- data Rec v = Rec+-- { foo :: Int -- this field exists in all versions+-- , bar :: Since V2 v Bool -- this field has been introduced in V2+-- , baz :: Until V2 v Double -- this field has been removed in V3+-- }+--+-- Besides reducing the number of data declarations,+-- this approach also has other advantages:+-- * It makes migrations declarative and self-documenting.+-- * It allows for less verbose version-upcasting functions,+-- since the fields that have a non-parametric type do not need to be copied.+-- * It is a foundation on which other useful abstractions can be built.+-- However there are also some drawbacks to consider:+-- some classes may require a separate standalone deriving clause for each version.+-- Moreover the usage of type-level computations can make the error messages+-- harder to understand.+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+module Versioning.Base+ ( V(..)+ , Since+ , Until+ , NA+ , na+ , V1+ , V2+ , V3+ , V4+ , V5+ , V6+ , V7+ , V8+ , V9+ , V10+ , V11+ , V12+ , V13+ , V14+ , V15+ , V16+ , V17+ , V18+ , V19+ , V20+ )+where++import Data.Aeson (FromJSON (..), ToJSON (..), Value (..))+import GHC.Generics (Generic)+import GHC.TypeNats++-- | The version of a data model+newtype V = V Nat++-- | This allows us to express that a field is only present since+-- a given version.+-- The first parameter is the version in which the field has been introduced,+-- the second parameter is the actual version of the data-type.+type family Since (s :: V) (v :: V) a :: * where+ Since ('V 1) ('V v) a = a+ Since ('V s) ('V 1) a = NA+ Since ('V s) ('V v) a = Since ('V (s - 1)) ('V (v - 1)) a++-- | This allows us to express that a field is only present until+-- a given version.+-- The first parameter is the last version in which the field is present,+-- the second parameter is the actual version of the data-type.+type family Until (u :: V) (v :: V) a :: * where+ Until ('V u) ('V 1) a = a+ Until ('V 1) ('V v) a = NA+ Until ('V u) ('V v) a = Until ('V (u - 1)) ('V (v - 1)) a++-- | A type indicating absence.+-- The 'Maybe' is a hack needed to let aeson parse a record even+-- if a field is missing.+type NA = Maybe Bare++-- | A placeholder for an absent value.+na :: NA+na = Nothing++-- | An uninhabited type.+-- We define our own type instead of using "Data.Void"+-- because we need additional instances.+-- Moreover this type is hidden to force users to use 'NA',+-- which is needed because of the 'Maybe' hack.+data Bare++deriving instance Eq Bare++deriving instance Generic Bare++deriving instance Show Bare++-- Attempting to supply a value for an absent field must produce a+-- parsing failure+instance FromJSON Bare where+ parseJSON _ = fail "An NA field should be absent or null"++-- We provide an instance to make the compiler happy.+-- In practice it will never be used.+instance ToJSON Bare where+ toJSON _ = Null++-- Handy type synonyms to minimize parenthesis+type V1 = 'V 1++type V2 = 'V 2++type V3 = 'V 3++type V4 = 'V 4++type V5 = 'V 5++type V6 = 'V 6++type V7 = 'V 7++type V8 = 'V 8++type V9 = 'V 9++type V10 = 'V 10++type V11 = 'V 11++type V12 = 'V 12++type V13 = 'V 13++type V14 = 'V 14++type V15 = 'V 15++type V16 = 'V 16++type V17 = 'V 17++type V18 = 'V 18++type V19 = 'V 19++type V20 = 'V 20
+ src/Versioning/JSON.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeInType #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+module Versioning.JSON+ ( Applied+ , DecodeAnyVersion+ , WithAnyVersion+ , decodeAnyVersion+ , withAnyVersion+ , withAnyVersionM+ )+where++import Control.Applicative ((<|>))+import Data.Aeson (FromJSON, decode)+import qualified Data.ByteString.Lazy as LazyBS+import Data.Functor.Identity (Identity (..))+import Data.Kind (Constraint, Type)+import GHC.TypeLits++import Versioning.Base+import Versioning.Upgrade++-- | Decode a JSON string by trying all the versions decrementally+-- and upgrade the decoded object to the newest version.+decodeAnyVersion+ :: forall v a . DecodeAnyVersion v v a => LazyBS.ByteString -> Maybe (a v)+decodeAnyVersion = decodeAnyVersion' @v @v++-- | Decode a JSON string by trying all the versions decrementally+-- and apply an action to the decoded object at its original version.+withAnyVersionM+ :: forall c v a m+ . (WithAnyVersion v a c, Applicative m, c a v)+ => ApplyM m a c+ -> LazyBS.ByteString+ -> m (Maybe (Applied c a))+withAnyVersionM = withAnyVersion' @v @a @c++-- | Pure version of 'withAnyVersionM'.+withAnyVersion+ :: forall c v a+ . (WithAnyVersion v a c, c a v)+ => Apply a c+ -> LazyBS.ByteString+ -> Maybe (Applied c a)+withAnyVersion action = runIdentity . withAnyVersionM @c @v @a (Identity . action)++-- | The result type of the action that has been applied to the decoded object+-- with 'withAnyVersion'.+type family Applied (c :: (V -> Type) -> V -> Constraint) (a :: V -> Type) :: Type++type ApplyM m a c = forall v. c a v => a v -> m (Applied c a)++type Apply a c = forall v. c a v => a v -> Applied c a++class DecodeAnyVersion (v :: V) (w :: V) (a :: V -> Type) where+ decodeAnyVersion' :: LazyBS.ByteString -> Maybe (a w)++instance {-# OVERLAPPING #-} (FromJSON (a V1), Upgrade V1 w a) => DecodeAnyVersion V1 w a where+ decodeAnyVersion' bs = upgrade @V1 @w <$> decode @(a V1) bs++instance {-# OVERLAPPABLE #-} (DecodeAnyVersion (Decr v V1) w a, FromJSON (a v), FromJSON (a (Decr v V1)), Upgrade v w a)+ => DecodeAnyVersion v w a where+ decodeAnyVersion' bs = upgrade @v @w <$> decode @(a v) bs+ <|> decodeAnyVersion' @(Decr v V1) @w bs++class WithAnyVersion (v :: V) (a :: V -> Type) c where+ withAnyVersion' :: (Applicative m, c a v) => ApplyM m a c -> LazyBS.ByteString -> m (Maybe (Applied c a))++instance {-# OVERLAPPING #-} (FromJSON (a V1), c a V1) => WithAnyVersion V1 a c where+ withAnyVersion' action bs = case decode @(a V1) bs of+ Just doc -> Just <$> action doc+ Nothing -> pure Nothing++instance {-# OVERLAPPABLE #-} (WithAnyVersion (Decr v V1) a c, FromJSON (a v), FromJSON (a (Decr v V1)), c a v, c a (Decr v V1))+ => WithAnyVersion v a c where+ withAnyVersion' action bs = case decode @(a v) bs of+ Just doc -> Just <$> action doc+ Nothing -> withAnyVersion' @(Decr v V1) @a @c action bs++-- | Decrement version until the target version is reached+type family Decr (c :: V) (v :: V) :: V where+ Decr ('V c) ('V v) = Decr' (CmpNat c v) ('V c)++type family Decr' (o :: Ordering) (c :: V) :: V where+ Decr' 'GT ('V c) = 'V (c - 1)+ Decr' 'EQ ('V c) = 'V c+ Decr' 'LT ('V c) = TypeError+ ( 'Text "Cannot decrement "+ ':<>: 'ShowType ('V c)+ ':<>: 'Text " because the target is a higher version"+ )
+ src/Versioning/Upgrade.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+module Versioning.Upgrade+ ( Adapt(..)+ , Upgrade(..)+ , upgrade+ )+where++import Data.Kind (Type)+import GHC.TypeLits++import Versioning.Base++-- | Adapt from a version to another+class Adapt (v :: V) (w :: V) (a :: V -> Type) where+ adapt :: a v -> a w++-- | Upgrade from a lower to a higher version by calling 'adapt' on all+-- the intermediary steps.+upgrade :: Upgrade v w a => a v -> a w+upgrade = upgrade'++-- | Upgrade from a lower to a higher version by calling 'adapt' on all+-- the intermediary steps.+-- You do not need to define any instance.+-- They are derived automatically if all the intermediary+-- 'Adapt' instances are defined.+class Upgrade (v :: V) (w :: V) (a :: V -> Type) where+ upgrade' :: a v -> a w++instance {-# OVERLAPS #-} Upgrade v v a where+ upgrade' x = x++instance {-# OVERLAPPABLE #-} (Adapt v (Incr v w) a, Upgrade (Incr v w) w a)+ => Upgrade v w a where+ upgrade' x = upgrade' @(Incr v w) @w (adapt @v @(Incr v w) x)++-- | Increment version until the target version is reached+type family Incr (c :: V) (v :: V) :: V where+ Incr ('V c) ('V v) = Incr' (CmpNat c v) ('V c)++type family Incr' (o :: Ordering) (c :: V) :: V where+ Incr' 'LT ('V c) = 'V (c + 1)+ Incr' 'EQ ('V c) = 'V c+ Incr' 'GT ('V c) = TypeError+ ( 'Text "Cannot increment "+ ':<>: 'ShowType ('V c)+ ':<>: 'Text " because the target is a lower version"+ )
+ tests/Main.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE UndecidableInstances #-}+module Main where++import Data.Aeson+import qualified Data.ByteString.Lazy as LazyBS+import GHC.Generics (Generic)+import Test.Hspec+import Versioning.Base+import Versioning.JSON+import Versioning.Upgrade++main :: IO ()+main = hspec $ do+ describe "Upgrade" $ do+ it "Can upgrade across two versions" $+ upgrade @V1 foo1 `shouldBe` foo3++ describe "DecodeAnyVersion" $ do+ it "Can decode from V1" $+ decodeAnyVersion @V3 fooJsonV1 `shouldBe` Just foo3++ describe "DecodeAnyVersion" $ do+ it "Can decode from V3" $+ decodeAnyVersion @V3 fooJsonV3 `shouldBe` Just foo3++ describe "WithAnyVersion" $ do+ -- Decode a Foo and return its string representation without upgrading it+ it "Can apply a function on the decoded object" $ do+ let Just res = withAnyVersion @ShowAnyVersion @V3 @Foo showAnyVersion fooJsonV1+ res `shouldBe` show foo1++data Foo v = Foo+ { always :: Int -- this field exists in all versions+ , sinceV2 :: Since V2 v Bool -- this field has been introduced in V2+ , sinceV3 :: Since V3 v String -- this field has been introduced in V3+ , untilV2 :: Until V2 v Double -- this field has been removed in V3+ } deriving (Generic)++deriving instance Eq (Foo V1)++deriving instance Eq (Foo V2)++deriving instance Eq (Foo V3)++instance FromJSON (Foo V1)++instance FromJSON (Foo V2)++instance FromJSON (Foo V3)++deriving instance Show (Foo V1)++deriving instance Show (Foo V2)++deriving instance Show (Foo V3)++-- How to upcast from V1 to V2+instance Adapt V1 V2 Foo where+ adapt foo = foo { sinceV2 = True+ , sinceV3 = na+ , untilV2 = untilV2 foo+ }++-- How to upcast from V2 to V3+instance Adapt V2 V3 Foo where+ adapt foo = foo { sinceV2 = sinceV2 foo+ , sinceV3 = "hello"+ , untilV2 = na+ }++type instance Applied ShowAnyVersion a = String++-- | Show an object, whatever its version+class ShowAnyVersion (a :: V -> *) (v :: V) where+ showAnyVersion :: a v -> String++instance Show (Foo v) => ShowAnyVersion Foo v where+ showAnyVersion = show++-- | A 'Foo' at version V1+foo1 :: Foo V1+foo1 = Foo+ { always = 1+ , sinceV2 = na+ , sinceV3 = na+ , untilV2 = 3.14+ }++-- | A 'Foo' at version V3+foo3 :: Foo V3+foo3 = Foo+ { always = 1+ , sinceV2 = True+ , sinceV3 = "hello"+ , untilV2 = Nothing+ }++fooJsonV1 :: LazyBS.ByteString+fooJsonV1 = "{\"always\":1, \"untilV2\": 3.14}"++fooJsonV3 :: LazyBS.ByteString+fooJsonV3 = "{\"always\":1, \"sinceV2\": true, \"sinceV3\": \"hello\"}"
+ versioning.cabal view
@@ -0,0 +1,38 @@+name: versioning+version: 0.1.0.0+synopsis: Type-safe data versioning.+description: This package provides various tools to deal with+ data versioning in a type-safe way.+homepage: https://github.com/lortabac/versioning+license: Apache-2.0+license-file: LICENSE+author: Lorenzo Tabacchini+maintainer: lortabac@gmx.com+copyright: (c) 2018 Lorenzo Tabacchini+category: Data+build-type: Simple+extra-source-files: ChangeLog.md+cabal-version: >=1.10++library+ exposed-modules: Versioning.Base+ , Versioning.Upgrade+ , Versioning.JSON+ build-depends: base >=4.11 && <4.12+ , aeson >=1.0 && <1.5+ , bytestring >=0.10 && <0.11+ hs-source-dirs: src+ default-language: Haskell2010+ ghc-options: -Wall++test-suite tests+ type: exitcode-stdio-1.0+ main-is: Main.hs+ build-depends: base >=4.11 && <4.12+ , aeson >=1.0 && <1.5+ , bytestring >=0.10 && <0.11+ , hspec >=2.0 && <2.6+ , versioning+ hs-source-dirs: tests+ default-language: Haskell2010+ ghc-options: -Wall