composable-associations (empty) → 0.1.0.0
raw patch · 6 files changed
+254/−0 lines, 6 filesdep +basedep +composable-associationsdep +lenssetup-changed
Dependencies added: base, composable-associations, lens, tasty, tasty-hunit
Files
- LICENSE +30/−0
- README.md +13/−0
- Setup.hs +2/−0
- composable-associations.cabal +40/−0
- src/Data/ComposableAssociation.hs +126/−0
- test/Main.hs +43/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Samuel Protas (c) 2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Samuel Protas nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,13 @@+# composable-associations++[](https://travis-ci.org/SamProtas/composable-associations)+[](http://en.wikipedia.org/wiki/BSD_licenses)+[](http://haskell.org)+[](https://hackage.haskell.org/package/composable-associations)+++This package defines basic types, helper functions and Lens's. You'll probably want to import a more specific version of+this library (like composable-associations-aeson) to get access to some useful type class instances.++Documentation available on Hackage.+
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ composable-associations.cabal view
@@ -0,0 +1,40 @@+name: composable-associations+version: 0.1.0.0+synopsis: Types and helpers for composing types into a single larger key-value type.+description:+ A library providing generic types and helpers for composing types together into a a single key-value type.+ .+ This is useful when a normalized data model has a denormalized serialization format. Using this libraries types and+ functions you build compose your data into the denormalized key-value format needed for serialization. Other+ libraries provide concrete implementations for a given serialization format.+homepage: https://github.com/SamProtas/composable-associations#readme+license: BSD3+license-file: LICENSE+author: Sam Protas+maintainer: sam.protas@gmail.com+copyright: 2017 Samuel Protas+category: Web+build-type: Simple+extra-source-files: README.md+cabal-version: >=1.10++library+ hs-source-dirs: src+ exposed-modules: Data.ComposableAssociation+ build-depends: base >= 4.7 && < 5+ default-language: Haskell2010++test-suite tests+ default-language: Haskell2010+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Main.hs+ build-depends: base >= 4.7 && < 5+ , composable-associations+ , lens+ , tasty+ , tasty-hunit++source-repository head+ type: git+ location: https://github.com/SamProtas/composable-associations/tree/master/composable-associations
+ src/Data/ComposableAssociation.hs view
@@ -0,0 +1,126 @@+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveFoldable #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE ExistentialQuantification #-}+module Data.ComposableAssociation+ ( -- * Description+ -- $header++ -- * Core Types+ Association (..)+ , (:<>) (..)+ , WithAssociation++ -- * Helper Functions+ , withAssociation+ , asValue+ , reKey++ -- * Lens+ , _value+ , _assoc+ , _base++ -- * Generic Invalid Encoding Exception+ , ObjectEncodingException (..)+ ) where++import GHC.Generics+import Data.Proxy+import Control.Exception+import Data.Typeable++-- | A type representing a key-value association where the "key" itself exists only at the type level.+--+-- >>> let x = Association Proxy [1, 2, 3] :: Asssociation "type-level-key" [Int]+--+-- This type exists primarily as a way to "tag" data with a key for the purpose of serializing haskell data into+-- formats that have a key-value representation (ex: a JSON object).+--+-- The example above represents a serializable key-value pair with a key of @"type-level-key"@ and a value of @[1, 2, 3]@.+--+-- Storing the key as type-level information allows for unambiguous deserialization.+data Association key value = Association (Proxy key) value+ deriving (Show, Eq, Generic, Functor, Foldable, Traversable)++-- | A type representing the composition of a base type (which can be serialized into a key-value structure) along with a key-value type.+--+-- This type exists as a way to compose a haskell value that has a key-value representation (ex: a haskell+-- record where its fields are keys to their values) with additional key-value associations into a single key-value+-- object.+--+-- This is intended for use with @Association@ to add additional key-values to a type for the purposes of+-- serialization/deserialization.+--+-- For example:+--+-- >>> data User = User { name :: String, age :: Int }+-- >>> let alice = User "Alice" 26+-- >>> let bob = User "Bob" 25+-- >>> let charlie = User "Charlie" 27+-- >>> let bobsFriends = [alice, charlie]+-- >>> bobAndFriends :: User :<> Association "friends" [User]+-- >>> let bobAndFriends = bob :<> Association Proxy bobsFriends+--+-- While @(bob, bobsFriends)@ contains the same values as @bobAndFriends@, it lacks information about how to combine+-- @bob@ and @bobsFriends@ together into a single serialized key-value object (as well as how to deserialize+-- that back into haskell values).+data base :<> assoc = base :<> assoc+ deriving (Show, Eq, Generic, Functor, Foldable, Traversable)++-- | Type alias for the (:<>) type operator.+--+-- Useful if you don't like the TypeOperators extension.+type WithAssociation base assoc = base :<> assoc++-- | Function alias for the @(:<>)@ type constructor.+withAssociation :: a -> b -> WithAssociation a b+withAssociation = (:<>)++-- | @_value :: Lens value value' (Association key value) (Association key value')@+_value :: Functor f => (value -> f value') -> Association key value -> f (Association key value')+_value inj (Association key value) = Association key <$> inj value++-- | @_assoc :: Lens assoc assoc' (base :<> assoc) (base :<> assoc')@+_assoc :: Functor f => (assoc -> f assoc') -> base :<> assoc -> f (base :<> assoc')+_assoc inj (base :<> keyValue) = (:<>) base <$> inj keyValue++-- | @_base :: Lens base base' (base :<> assoc) (base' :<> assoc)@+_base :: Functor f => (base -> f base') -> base :<> assoc -> f (base' :<> assoc)+_base inj (base :<> assoc) = flip (:<>) assoc <$> inj base++-- | Convenience function for creating associations.+--+-- This is especially useful when type-inference elsewhere in your program will determine the type of the Association.+--+-- >>> let x = asValue True :: Association "whatever-key" Bool+asValue :: obj -> Association key obj+asValue = Association Proxy++-- | Convenience function for changing the type of the @Association@'s key.+--+-- >>> let x = Association Proxy 10 :: Association "key-x" Int+-- >>> let y = reKey x :: Association "key-y" Int+reKey :: Association key obj -> Association key' obj+reKey (Association _ obj) = Association Proxy obj++-- | Generic encoding exception for when a @:<>@ "base" cannot be encoded as something object-like.+--+-- Each serialization should have a more specific version of this exception to convey information about the failure.+data ObjectEncodingException = forall e. Exception e => ObjectEncodingException e deriving Typeable++instance Show ObjectEncodingException where+ show (ObjectEncodingException e) = show e++instance Exception ObjectEncodingException++-- $header+-- This library exports core types, helper functions, and Lens's.+--+-- Unless you're implementing a serialization library (orphan instances for these types to implement+-- serialization/deserialization for some format) you probably don't want to import this package directly. Additional+-- packages in this namespace re-export this module along with their orphan instances for serialization.
+ test/Main.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeOperators #-}+module Main where++import Test.Tasty+import Test.Tasty.HUnit+import Control.Lens+import Data.Proxy++import Data.ComposableAssociation++main :: IO ()+main = defaultMain tests++tests :: TestTree+tests = testGroup "Tests" [unitTests]++unitTests :: TestTree+unitTests = testGroup "Unit Tests"+ [ testCase "Assoc Getter" $ setKeyTest ^. _value @?= "object"+ , testCase "Assoc Modify" $ (setKeyTest & _value %~ (++ "1")) @?= Association Proxy "object1"+ , testCase "Assoc Set" $ (setKeyTest & _value .~ True) @?= Association Proxy True+ , testCase ":<> Getter1" $ withObjTest ^. _base @?= "a"+ , testCase ":<> Modify1" $ (withObjTest & _base %~ (++ "1")) @?= "a1" :<> "b"+ , testCase ":<> Set1" $ (withObjTest & _base .~ True) @?= True :<> "b"+ , testCase ":<> Getter2" $ withObjTest ^. _assoc @?= "b"+ , testCase ":<> Modify2" $ (withObjTest & _assoc %~ (++ "1")) @?= "a" :<> "b1"+ , testCase ":<> Set2" $ (withObjTest & _assoc .~ True) @?= "a" :<> True+ , testCase "Assoc :<> Assoc Base-Getter" $ withSetKeyTest ^. _base . _value @?= "a"+ , testCase "Assoc :<> Assoc Key-Getter" $ withSetKeyTest ^. _assoc . _value @?= "b"+ , testCase "Assoc :<> Assoc Base-Modify" $+ (withSetKeyTest & _base . _value %~ (++ "1")) @?= asValue "a1" :<> asValue "b"+ , testCase "Assoc :<> Assoc Key-Modify" $+ (withSetKeyTest & _assoc . _value %~ (++ "1")) @?= asValue "a" :<> asValue "b1" ]++setKeyTest :: Association "test" String+setKeyTest = asValue "object"++withObjTest :: String :<> String+withObjTest = "a" :<> "b"++withSetKeyTest :: Association "test_key_1" String :<> Association "test_key_2" String+withSetKeyTest = asValue "a" :<> asValue "b"