packages feed

config-schema 0.2.0.0 → 0.3.0.0

raw patch · 6 files changed

+341/−133 lines, 6 filesdep +config-schemadep +prettydep ~basedep ~config-valuedep ~kan-extensionsPVP ok

version bump matches the API change (PVP)

Dependencies added: config-schema, pretty

Dependency ranges changed: base, config-value, kan-extensions, semigroupoids, text

API changes (from Hackage documentation)

- Config.Schema.Load: UnusedSections :: [Text] -> Problem
- Config.Schema.Load: instance GHC.Classes.Eq Config.Schema.Load.LoadError
- Config.Schema.Load: instance GHC.Classes.Ord Config.Schema.Load.LoadError
- Config.Schema.Spec: MkSectionSpecs :: (Ap SectionSpec a) -> SectionSpecs a
- Config.Schema.Spec: MkValueSpecs :: Compose NonEmpty (Coyoneda ValueSpec) a -> ValueSpecs a
- Config.Schema.Spec: [unValueSpecs] :: ValueSpecs a -> Compose NonEmpty (Coyoneda ValueSpec) a
- Config.Schema.Spec: newtype SectionSpecs a
- Config.Schema.Spec: newtype ValueSpecs a
+ Config.Schema.Load: UnusedSection :: Text -> Problem
+ Config.Schema.Spec: [AssocSpec] :: ValueSpecs a -> ValueSpec [(Text, a)]
+ Config.Schema.Spec: assocSpec :: ValueSpecs a -> ValueSpecs [(Text, a)]
+ Config.Schema.Spec: data SectionSpecs a
+ Config.Schema.Spec: data ValueSpecs a
- Config.Schema.Docs: generateDocs :: ValueSpecs a -> Text
+ Config.Schema.Docs: generateDocs :: ValueSpecs a -> Doc
- Config.Schema.Load: LoadError :: [Text] -> Problem -> LoadError
+ Config.Schema.Load: LoadError :: Position -> [Text] -> Problem -> LoadError
- Config.Schema.Load: loadValue :: ValueSpecs a -> Value -> Either (NonEmpty LoadError) a
+ Config.Schema.Load: loadValue :: ValueSpecs a -> Value Position -> Either (NonEmpty LoadError) a
- Config.Schema.Spec: optSection' :: Text -> Text -> ValueSpecs a -> SectionSpecs (Maybe a)
+ Config.Schema.Spec: optSection' :: Text -> ValueSpecs a -> Text -> SectionSpecs (Maybe a)
- Config.Schema.Spec: reqSection' :: Text -> Text -> ValueSpecs a -> SectionSpecs a
+ Config.Schema.Spec: reqSection' :: Text -> ValueSpecs a -> Text -> SectionSpecs a
- Config.Schema.Spec: runValueSpecs :: Functor f => (forall x. ValueSpec x -> f x) -> ValueSpecs a -> NonEmpty (f a)
+ Config.Schema.Spec: runValueSpecs :: Alt f => (forall x. ValueSpec x -> f x) -> ValueSpecs a -> f a
- Config.Schema.Spec: runValueSpecs_ :: (forall x. ValueSpec x -> m) -> ValueSpecs a -> NonEmpty m
+ Config.Schema.Spec: runValueSpecs_ :: Semigroup m => (forall x. ValueSpec x -> m) -> ValueSpecs a -> m

Files

ChangeLog.md view
@@ -1,5 +1,12 @@ # Revision history for config-schema +## 0.3.0.0  -- 2017-05-09++* Added "association list" specifications+* Use `pretty` library for documentation generation+* Reorder parameters so that documentation comes last+* Hide implementations of `ValueSpecs` and `SectionSpecs`+ ## 0.2.0.0  -- 2017-05-07  * Expose `liftValueSpec` and `liftSectionSpec`
config-schema.cabal view
@@ -1,5 +1,5 @@ name:                config-schema-version:             0.2.0.0+version:             0.3.0.0 synopsis:            Schema definitions for the config-value package description:         This package makes it possible to defined schemas for use when                      loading configuration files using the config-value format.@@ -17,6 +17,7 @@ cabal-version:       >=1.10 homepage:            https://github.com/glguy/config-schema bug-reports:         https://github.com/glguy/config-schema/issues+tested-with:         GHC==8.0.2  source-repository head   type:                 git@@ -24,14 +25,23 @@  library   exposed-modules:      Config.Schema, Config.Schema.Docs, Config.Schema.Load, Config.Schema.Spec-  build-depends:        base           >=4.9 && <4.11,-                        text           >=1.2 && <1.3,-                        free           >=4.12 && <4.13,-                        kan-extensions >=5.0 && <5.1,-                        semigroupoids  >=5.2 && <5.3,-                        transformers   >=0.5 && <0.6,-                        config-value   >=0.5 && <0.6,-                        containers     >=0.5 && <0.6+  build-depends:        base           >=4.9   && <4.11,+                        config-value   >=0.6   && <0.7,+                        containers     >=0.5   && <0.6,+                        free           >=4.12  && <4.13,+                        kan-extensions >=5.0.2 && <5.1,+                        pretty         >=1.1.3 && <1.2,+                        semigroupoids  >=5.2   && <5.3,+                        text           >=1.2   && <1.3,+                        transformers   >=0.5   && <0.6   hs-source-dirs:       src+  default-language:     Haskell2010+  ghc-options:          -Wall++test-suite unit-tests+  type:                 exitcode-stdio-1.0+  main-is:              Main.hs+  hs-source-dirs:       tests+  build-depends:        base, config-value, config-schema, text   default-language:     Haskell2010   ghc-options:          -Wall
src/Config/Schema/Docs.hs view
@@ -36,93 +36,100 @@   ( generateDocs   ) where +import           Data.List (intersperse) import           Data.Map (Map) import qualified Data.Map as Map-import           Data.Monoid-import           Data.List.NonEmpty (NonEmpty)-import qualified Data.List.NonEmpty as NonEmpty import           Data.Text (Text) import qualified Data.Text as Text+import           Text.PrettyPrint (Doc, text, ($+$), (<>), (<+>), nest, empty, hsep)  import           Config.Schema.Spec  -- | Default documentation generator. This generator is specifically -- for configuration specifications where the top-level specification -- is named with the empty string (@""@).-generateDocs :: ValueSpecs a -> Text-generateDocs spec = Text.unlines-  ("Configuration file fields:"-   : map ("    " <>) top-  ++ concatMap sectionLines (Map.toList m'))-+generateDocs :: ValueSpecs a -> Doc+generateDocs spec+  = vcat'+  $ txt "Configuration file fields:"+  : nest 4 top+  : concatMap sectionLines (Map.toList m')   where     topname = ""     Just top = Map.lookup topname m-    DocBuilder (m,"") = valuesDoc spec+    (m,"") = runDocBuilder (valuesDoc spec)     m' = Map.delete topname m -    sectionLines :: (Text, [Text]) -> [Text]-    sectionLines (name, fields)-      = ""-      : name-      : map ("    "<>) fields+    sectionLines :: (Text, Doc) -> [Doc]+    sectionLines (name, fields) = [text "", txt name, nest 4 fields]   -- | Compute the documentation for a list of sections, store the -- documentation in the sections map and return the name of the section.-sectionsDoc :: Text -> SectionSpecs a -> DocBuilder Text-sectionsDoc l spec = emitDoc l =<< runSections_ sectionDoc spec+sectionsDoc :: Text -> SectionSpecs a -> DocBuilder Doc+sectionsDoc l spec = emitDoc l . vcat' =<< runSections_ (fmap pure . sectionDoc) spec   -- | Compute the documentation lines for a single key-value pair.-sectionDoc :: SectionSpec a -> DocBuilder [Text]+sectionDoc :: SectionSpec a -> DocBuilder Doc sectionDoc s =   case s of-    ReqSection name desc w -> aux "REQUIRED " name desc <$> valuesDoc w-    OptSection name desc w -> aux ""          name desc <$> valuesDoc w+    ReqSection name desc w -> aux "REQUIRED" name desc <$> valuesDoc w+    OptSection name desc w -> aux empty      name desc <$> valuesDoc w   where     aux req name desc val =-      (name <> ": " <> req <> val)-      : if Text.null desc then [] else ["   " <> desc]+      txt name <> ":" <+> req <+> val $+$+      if Text.null desc then empty else nest 4 (txt desc)   -- | Compute the documentation line for a particular value specification. -- Any sections contained in the specification will be stored in the -- sections map.-valuesDoc :: ValueSpecs a -> DocBuilder Text-valuesDoc = fmap disjunction . sequenceA . runValueSpecs_ valueDoc+valuesDoc :: ValueSpecs a -> DocBuilder Doc+valuesDoc = fmap disjunction . sequenceA . runValueSpecs_ (fmap pure valueDoc)   -- | Combine a list of text with the word @or@.-disjunction :: NonEmpty Text -> Text-disjunction = Text.intercalate " or " . NonEmpty.toList+disjunction :: [Doc] -> Doc+disjunction = hsep . intersperse "or"   -- | Compute the documentation fragment for an individual value specification.-valueDoc :: ValueSpec a -> DocBuilder Text+valueDoc :: ValueSpec a -> DocBuilder Doc valueDoc w =   case w of-    TextSpec         -> return "text"-    IntegerSpec      -> return "integer"-    RationalSpec     -> return "number"-    AtomSpec a       -> return ("`" <> a <> "`")-    AnyAtomSpec      -> return "atom"+    TextSpec         -> pure "text"+    IntegerSpec      -> pure "integer"+    RationalSpec     -> pure "number"+    AtomSpec a       -> pure ("`" <> txt a <> "`")+    AnyAtomSpec      -> pure "atom"     SectionSpecs l s -> sectionsDoc l s-    NamedSpec    l s -> emitDoc l . pure =<< valuesDoc s-    CustomSpec l w'  -> ((l <> " ") <>) <$> valuesDoc w'-    ListSpec ws      -> ("list of " <>) <$> valuesDoc ws+    NamedSpec    l s -> emitDoc l =<< valuesDoc s+    CustomSpec l w'  -> (txt l                 <+>) <$> valuesDoc w'+    ListSpec ws      -> ("list of"             <+>) <$> valuesDoc ws+    AssocSpec ws     -> ("association list of" <+>) <$> valuesDoc ws   -- | A writer-like type. A mapping of section names and documentation -- lines is accumulated.-newtype DocBuilder a = DocBuilder (Map Text [Text], a)+newtype DocBuilder a = DocBuilder { runDocBuilder :: (Map Text Doc, a) }   deriving (Functor, Applicative, Monad, Monoid, Show)   -- | Given a section name and section body, store the body -- in the map of sections and return the section name. emitDoc ::-  Text   {- ^ section name -} ->-  [Text] {- ^ section body -} ->-  DocBuilder Text-emitDoc l xs = DocBuilder (Map.singleton l xs, l)+  Text {- ^ section name -} ->+  Doc  {- ^ section body -} ->+  DocBuilder Doc+emitDoc l xs = DocBuilder (Map.singleton l xs, txt l)++------------------------------------------------------------------------++-- | Like text, but works on Text values.+txt :: Text -> Doc+txt = text . Text.unpack++-- | Like vcat but using ($+$) instead of ($$) to avoid overlap.+vcat' :: [Doc] -> Doc+vcat' = foldr ($+$) empty
src/Config/Schema/Load.hs view
@@ -18,16 +18,17 @@   , Problem(..)   ) where -import           Control.Monad                    (unless, zipWithM)+import           Control.Monad                    (zipWithM) import           Control.Monad.Trans.Class        (lift) import           Control.Monad.Trans.State        (StateT(..), runStateT) import           Control.Monad.Trans.Except       (Except, runExcept, throwE) import           Control.Monad.Trans.Reader       (ReaderT, runReaderT, ask, local)+import           Data.Semigroup.Foldable          (asum1) import           Data.Functor.Alt                 (Alt((<!>))) import           Data.Monoid                      ((<>)) import           Data.Ratio                       (numerator, denominator)-import           Data.Semigroup.Foldable          (asum1) import           Data.List.NonEmpty               (NonEmpty)+import qualified Data.List.NonEmpty as NonEmpty import           Data.Text                        (Text) import qualified Data.Text as Text @@ -40,83 +41,94 @@ -- encountered. loadValue ::   ValueSpecs a                  {- ^ specification           -} ->-  Value                         {- ^ value                   -} ->+  Value Position                {- ^ value                   -} ->   Either (NonEmpty LoadError) a {- ^ errors or decoded value -} loadValue spec val = runLoad (getValue spec val)  -getSection :: SectionSpec a -> StateT [Section] Load a-getSection (ReqSection k _ w) =-  do v <- StateT (lookupSection k)+getSection :: Position -> SectionSpec a -> StateT [Section Position] Load a+getSection pos (ReqSection k _ w) =+  do v <- StateT (lookupSection pos k)      lift (scope k (getValue w v))-getSection (OptSection k _ w) =-  do mb <- optional1 (StateT (lookupSection k))+getSection pos (OptSection k _ w) =+  do mb <- optional1 (StateT (lookupSection pos k))      lift (traverse (scope k . getValue w) mb)  -getSections :: SectionSpecs a -> [Section] -> Load a-getSections p xs =-  do (a,leftover) <- runStateT (runSections getSection p) xs-     unless (null leftover) (loadFail (UnusedSections (map sectionName leftover)))-     return a+getSections :: Position -> SectionSpecs a -> [Section Position] -> Load a+getSections pos spec xs =+  do (a,leftovers) <- runStateT (runSections (getSection pos) spec) xs+     case NonEmpty.nonEmpty leftovers of+       Nothing -> return a+       Just ss -> asum1 (fmap (\s -> loadFail (sectionAnn s) (UnusedSection (sectionName s))) ss)  -getValue :: ValueSpecs a -> Value -> Load a-getValue s v = asum1 (runValueSpecs (getValue1 v) s)+getValue :: ValueSpecs a -> Value Position -> Load a+getValue s v = runValueSpecs (getValue1 v) s   -- | Match a primitive value specification against a single value.-getValue1 :: Value -> ValueSpec a -> Load a-getValue1 (Text t)       TextSpec           = pure t-getValue1 (Number _ n)   IntegerSpec        = pure n-getValue1 (Floating a b) IntegerSpec | Just i <- floatingToInteger a b = pure i-getValue1 (Number _ n)   RationalSpec       = pure (fromInteger n)-getValue1 (Floating a b) RationalSpec       = pure (floatingToRational a b)-getValue1 (List xs)      (ListSpec w)       = getList w xs-getValue1 (Atom b)       AnyAtomSpec        = pure (atomName b)-getValue1 (Atom b)       (AtomSpec a) | a == atomName b = pure ()-getValue1 (Sections s)   (SectionSpecs _ w) = getSections w s-getValue1 v              (NamedSpec _ w)    = getValue w v-getValue1 v              (CustomSpec l w)   = getCustom l w v+getValue1 :: Value Position -> ValueSpec a -> Load a+getValue1 (Text _ t)       TextSpec           = pure t+getValue1 (Number _ _ n)   IntegerSpec        = pure n+getValue1 (Floating _ a b) IntegerSpec | Just i <- floatingToInteger a b = pure i+getValue1 (Number _ _ n)   RationalSpec       = pure (fromInteger n)+getValue1 (Floating _ a b) RationalSpec       = pure (floatingToRational a b)+getValue1 (List _ xs)      (ListSpec w)       = getList w xs+getValue1 (Atom _ b)       AnyAtomSpec        = pure (atomName b)+getValue1 (Atom _ b)       (AtomSpec a) | a == atomName b = pure ()+getValue1 (Sections p s)   (SectionSpecs _ w) = getSections p w s+getValue1 (Sections _ s)   (AssocSpec w)      = getAssoc w s+getValue1 v                (NamedSpec _ w)    = getValue w v+getValue1 v                (CustomSpec l w)   = getCustom l w v -getValue1 _              TextSpec           = loadFail (SpecMismatch "text")-getValue1 _              IntegerSpec        = loadFail (SpecMismatch "integer")-getValue1 _              RationalSpec       = loadFail (SpecMismatch "number")-getValue1 _              ListSpec{}         = loadFail (SpecMismatch "list")-getValue1 _              AnyAtomSpec        = loadFail (SpecMismatch "atom")-getValue1 _              (AtomSpec a)       = loadFail (SpecMismatch ("`" <> a <> "`"))-getValue1 _              (SectionSpecs l _) = loadFail (SpecMismatch l)+getValue1 v TextSpec           = loadFail (valueAnn v) (SpecMismatch "text")+getValue1 v IntegerSpec        = loadFail (valueAnn v) (SpecMismatch "integer")+getValue1 v RationalSpec       = loadFail (valueAnn v) (SpecMismatch "number")+getValue1 v ListSpec{}         = loadFail (valueAnn v) (SpecMismatch "list")+getValue1 v AnyAtomSpec        = loadFail (valueAnn v) (SpecMismatch "atom")+getValue1 v (AtomSpec a)       = loadFail (valueAnn v) (SpecMismatch ("`" <> a <> "`"))+getValue1 v (SectionSpecs l _) = loadFail (valueAnn v) (SpecMismatch l)+getValue1 v AssocSpec{}        = loadFail (valueAnn v) (SpecMismatch "association list")   -- | This operation processes all of the values in a list with the given -- value specification and updates the scope with a one-based list index.-getList :: ValueSpecs a -> [Value] -> Load [a]+getList :: ValueSpecs a -> [Value Position] -> Load [a] getList w = zipWithM (\i x -> scope (Text.pack (show i)) (getValue w x)) [1::Int ..]  +-- | This operation processes all of the values in a section list+-- against the given specification and associates them with the+-- section name.+getAssoc :: ValueSpecs a -> [Section Position] -> Load [(Text,a)]+getAssoc w = traverse $ \(Section _ k v) -> (,) k <$> getValue w v++ -- | Match a value against its specification. If 'Just' is matched -- return the value. If 'Nothing is matched, report an error. getCustom ::   Text                 {- ^ label         -} ->   ValueSpecs (Maybe a) {- ^ specification -} ->-  Value                {- ^ value         -} ->+  Value Position       {- ^ value         -} ->   Load a getCustom l w v =   do x <- getValue w v      case x of-       Nothing -> loadFail (SpecMismatch l)+       Nothing -> loadFail (valueAnn v) (SpecMismatch l)        Just y  -> pure y   -- | Extract a section from a list of sections by name. lookupSection ::-  Text                    {- ^ section name                       -} ->-  [Section]               {- ^ available sections                 -} ->-  Load (Value, [Section]) {- ^ found value and remaining sections -}-lookupSection key [] = loadFail (MissingSection key)-lookupSection key (s@(Section k v):xs)+  Position                    {- ^ starting position of sections      -} ->+  Text                        {- ^ section name                       -} ->+  [Section p]                 {- ^ available sections                 -} ->+  Load (Value p, [Section p]) {- ^ found value and remaining sections -}+lookupSection pos key [] = loadFail pos (MissingSection key)+lookupSection pos key (s@(Section _ k v):xs)   | key == k  = pure (v, xs)-  | otherwise = do (v',xs') <- lookupSection key xs+  | otherwise = do (v',xs') <- lookupSection pos key xs                    return (v',s:xs')  ------------------------------------------------------------------------@@ -149,8 +161,8 @@ -- | Type for errors that can be encountered while decoding a value according -- to a specification. The error includes a key path indicating where in -- the configuration file the error occurred.-data LoadError = LoadError [Text] Problem -- ^ path to problem and problem description-  deriving (Eq, Ord, Read, Show)+data LoadError = LoadError Position [Text] Problem -- ^ position, path, problem+  deriving (Read, Show)   -- | Run the Load computation until it produces a result or terminates@@ -161,9 +173,9 @@  -- | Problems that can be encountered when matching a 'Value' against a 'ValueSpecs'. data Problem-  = MissingSection Text   -- ^ missing section name-  | UnusedSections [Text] -- ^ unused section names-  | SpecMismatch Text     -- ^ failed specification name+  = MissingSection Text -- ^ missing section name+  | UnusedSection Text  -- ^ unused section names+  | SpecMismatch Text   -- ^ failed specification name   deriving (Eq, Ord, Read, Show)  -- | Push a new key onto the stack of nested fields.@@ -171,10 +183,10 @@ scope key (MkLoad m) = MkLoad (local (key:) m)  -- | Abort value specification matching with the given error.-loadFail :: Problem -> Load a-loadFail cause = MkLoad $+loadFail :: Position -> Problem -> Load a+loadFail pos cause = MkLoad $   do path <- ask-     lift (throwE (pure (LoadError (reverse path) cause)))+     lift (throwE (pure (LoadError pos (reverse path) cause)))  ------------------------------------------------------------------------ 
src/Config/Schema/Spec.hs view
@@ -1,5 +1,5 @@ {-# Language FlexibleInstances, RankNTypes, GADTs, KindSignatures #-}-{-# Language GeneralizedNewtypeDeriving, OverloadedStrings #-}+{-# Language DeriveFunctor, GeneralizedNewtypeDeriving, OverloadedStrings #-}  {-| Module      : Config.Schema.Spec@@ -22,16 +22,17 @@ module Config.Schema.Spec   (   -- * Specifying sections-    SectionSpecs(..)+    SectionSpecs   , reqSection   , optSection   , reqSection'   , optSection'    -- * Specifying values-  , ValueSpecs(..)+  , ValueSpecs   , Spec(..)   , sectionsSpec+  , assocSpec   , atomSpec   , anyAtomSpec   , listSpec@@ -63,11 +64,12 @@  import           Control.Applicative.Free         (Ap, runAp, runAp_, liftAp) import           Data.Functor.Const               (Const(..))-import           Data.Functor.Coyoneda            (Coyoneda(..), liftCoyoneda, lowerCoyoneda)-import           Data.Functor.Compose             (Compose(..), getCompose)+import           Data.Functor.Coyoneda            (Coyoneda(..), liftCoyoneda, lowerCoyoneda, hoistCoyoneda) import           Data.Functor.Alt                 (Alt(..)) import           Data.List.NonEmpty               (NonEmpty) import qualified Data.List.NonEmpty as NonEmpty+import           Data.Semigroup                   (Semigroup)+import           Data.Semigroup.Foldable          (asum1, foldMap1) import           Data.Text                        (Text) import qualified Data.Text as Text @@ -98,7 +100,7 @@  -- | Lift a single specification into a list of specifications. ----- @since 0.1.1.0+-- @since 0.2.0.0 liftSectionSpec :: SectionSpec a -> SectionSpecs a liftSectionSpec = MkSectionSpecs . liftAp @@ -144,10 +146,10 @@ -- | Specification for a required section with an explicit value specification. reqSection' ::   Text         {- ^ section name        -} ->-  Text         {- ^ description         -} ->   ValueSpecs a {- ^ value specification -} ->+  Text         {- ^ description         -} ->   SectionSpecs a-reqSection' n i w = liftSectionSpec (ReqSection n i w)+reqSection' n w i = liftSectionSpec (ReqSection n i w)   -- | Specification for an optional section with an implicit value specification.@@ -162,10 +164,10 @@ -- | Specification for an optional section with an explicit value specification. optSection' ::   Text         {- ^ section name        -} ->-  Text         {- ^ description         -} ->   ValueSpecs a {- ^ value specification -} ->+  Text         {- ^ description         -} ->   SectionSpecs (Maybe a)-optSection' n i w = liftSectionSpec (OptSection n i w)+optSection' n w i = liftSectionSpec (OptSection n i w)   ------------------------------------------------------------------------@@ -196,6 +198,10 @@   -- | Documentation identifier and section specification   SectionSpecs :: Text -> SectionSpecs a -> ValueSpec a +  -- | Matches an arbitrary list of sections. Similar to 'SectionSpec'+  -- except that that the section names are user-defined.+  AssocSpec :: ValueSpecs a -> ValueSpec [(Text,a)]+   -- | Documentation text, underlying specification   CustomSpec :: Text -> ValueSpecs (Maybe a) -> ValueSpec a @@ -203,38 +209,49 @@   NamedSpec :: Text -> ValueSpecs a -> ValueSpec a  + -- | Non-empty disjunction of value specifications. This type is the primary -- way to specify expected values. Use the 'Spec' class to generate 'ValueSpecs' -- for simple types. -- -- Multiple specifications can be combined using this type's 'Alt' instance.-newtype ValueSpecs a = MkValueSpecs { unValueSpecs :: Compose NonEmpty (Coyoneda ValueSpec) a }-  deriving Functor+newtype ValueSpecs a = MkValueSpecs { unValueSpecs :: NonEmpty (Coyoneda ValueSpec a) }+  deriving (Functor)  -- | Left-biased choice between two specifications instance Alt ValueSpecs where MkValueSpecs x <!> MkValueSpecs y = MkValueSpecs (x <!> y)   -- | Given an interpretation of a primitive value specification, extract a list of--- the possible interpretations of a disjunction of value specifications.------ Unlike 'runValueSpecs_', this allows the result of the interpretation to be indexed--- by the type of the primitive value specifications.-runValueSpecs :: Functor f => (forall x. ValueSpec x -> f x) -> ValueSpecs a -> NonEmpty (f a)-runValueSpecs f = fmap (lowerCoyoneda . hoistCoyoneda f) . getCompose . unValueSpecs+-- the possible interpretations of a disjunction of value specifications. Each of+-- these primitive interpretations will be combined using the provided 'Alt' instance.+runValueSpecs :: Alt f => (forall x. ValueSpec x -> f x) -> ValueSpecs a -> f a+runValueSpecs f = asum1 . fmap (runCoyoneda f) . unValueSpecs   -- | Given an interpretation of a primitive value specification, extract a list of--- the possible interpretations of a disjunction of value specifications.-runValueSpecs_ :: (forall x. ValueSpec x -> m) -> ValueSpecs a -> NonEmpty m-runValueSpecs_ f = fmap getConst . runValueSpecs (Const . f)+-- the possible interpretations of a disjunction of value specifications. Each of+-- these primitive interpretations will be combined using the provided 'Semigroup' instance.+runValueSpecs_ :: Semigroup m => (forall x. ValueSpec x -> m) -> ValueSpecs a -> m+runValueSpecs_ f = foldMap1 (runCoyoneda_ f) . unValueSpecs  +-- Helper for transforming the underlying type @f@ to one supporting a 'Functor'+-- instance before lowering.+runCoyoneda :: Functor g => (forall a. f a -> g a) -> Coyoneda f b -> g b+runCoyoneda f = lowerCoyoneda . hoistCoyoneda f++-- Helper for extracting the the value stored in a 'Coyoneda' while forgetting its+-- type index.+runCoyoneda_ :: (forall a. f a -> m) -> Coyoneda f b -> m+runCoyoneda_ f = getConst . runCoyoneda (Const . f)++ -- | Lift a primitive value specification to 'ValueSpecs'. ----- @since 0.1.1.0+-- @since 0.2.0.0 liftValueSpec :: ValueSpec a -> ValueSpecs a-liftValueSpec = MkValueSpecs . Compose . pure . liftCoyoneda+liftValueSpec = MkValueSpecs . pure . liftCoyoneda   ------------------------------------------------------------------------@@ -271,7 +288,7 @@  -- | Specification for matching any fractional number. ----- @since 0.1.1.0+-- @since 0.2.0.0 fractionalSpec :: Fractional a => ValueSpecs a fractionalSpec = fromRational <$> valuesSpec @@ -291,6 +308,17 @@ sectionsSpec i s = liftValueSpec (SectionSpecs i s)  +-- | Specification for a section list where the keys are user-defined.+-- Values are matched against the underlying specification and returned+-- as a list of section-name\/value pairs.+--+-- @since 0.3.0.0+assocSpec ::+  ValueSpecs a {- ^ underlying specification -} ->+  ValueSpecs [(Text,a)]+assocSpec = liftValueSpec . AssocSpec++ -- | Named value specification. This is useful for factoring complicated -- value specifications out in the documentation to avoid repetition of -- complex specifications.@@ -324,19 +352,12 @@  -- | Matches a non-empty list. ----- @since 0.1.1.0+-- @since 0.2.0.0 nonemptySpec :: ValueSpecs a -> ValueSpecs (NonEmpty a) nonemptySpec s = customSpec "nonempty" (listSpec s) NonEmpty.nonEmpty  -- | Matches a single element or a non-empty list. ----- @since 0.1.1.0+-- @since 0.2.0.0 oneOrNonemptySpec :: ValueSpecs a -> ValueSpecs (NonEmpty a) oneOrNonemptySpec s = pure <$> s <!> nonemptySpec s------------------------------------------------------------------------------ | Lift a natural transformation to be a natural transformation of--- 'Coyoneda'.-hoistCoyoneda :: (forall x. f x -> g x) -> (Coyoneda f a -> Coyoneda g a)-hoistCoyoneda f (Coyoneda g x) = Coyoneda g (f x)
+ tests/Main.hs view
@@ -0,0 +1,151 @@+{-# Language OverloadedStrings #-}+{-|+Module      : Main+Description : Unit tests for config-schema+Copyright   : (c) Eric Mertens, 2017+License     : ISC+Maintainer  : emertens@gmail.com+-}+module Main (main) where++import           Config+import           Config.Schema+import           Control.Applicative+import           Data.Foldable+import           Data.Text (Text)+import qualified Data.Text as Text++-- tests that are expected to pass.+--+-- The input sources are a list of lists of lines. Each outer list+-- element contains a list of lines representing a complete input+-- source. Each of these variations must pass the test.+test ::+  Show a =>+  Eq   a =>+  ValueSpecs a {- ^ specification to match -} ->+  a            {- ^ expected output        -} ->+  [[Text]]     {- ^ inputs sources         -} ->+  IO ()+test spec expected txtss =+  for_ txtss $ \txts ->+  case parse (Text.unlines txts) of+    Left e -> fail (show e)+    Right v ->+      case loadValue spec v of+        Left e -> fail (show e)+        Right x | x == expected -> return ()+                | otherwise -> fail ("Got " ++ show x ++ " but expected " +++                                     show expected)++main :: IO ()+main = sequenceA_++  [ test valuesSpec ("Hello world"::Text)+    [["\"Hello world\""]+    ,["\"H\\101l\\&l\\o157 \\"+     ,"  \\w\\x6frld\""]+    ]++  , test valuesSpec (1234::Integer)+    [["1234"]+    ,["1234.0"]+    ]++  , test valuesSpec (0.65::Rational)+    [["0.65e0"]+    ,["65e-2"]+    ,["6.5e-1"]+    ,["0.65"]+    ]++  , test anyAtomSpec "default"+    [["default"]]++  , test (atomSpec "testing-1-2-3") ()+    [["testing-1-2-3"]]++  , test (listSpec valuesSpec) ([]::[Integer])+    [["[]"]+    ,["[ ]"]]++  , test (listSpec anyAtomSpec) ["ḿyatoḿ"]+    [["[ḿyatoḿ]"]+    ,[" [ ḿyatoḿ ] "]+    ,["* ḿyatoḿ"]+    ]++  , test valuesSpec [1,2,3::Int]+    [["[1,2,3]"]+    ,["[1,2,3,]"]+    ,["* 1"+     ,"* 2"+     ,"* 3"]+    ]++  , test (listSpec valuesSpec) [[1,2],[3,4::Int]]+    [["[[1,2,],[3,4]]"]+    ,["*[1,2]"+     ,"*[3,4]"]+    ,["**1"+     ," *2"+     ,"* *3"+     ,"  *4"+     ]+    ]++  , test (assocSpec valuesSpec) ([]::[(Text,Int)])+    [["{}"]+    ,["{ }"]+    ]++  , test (assocSpec valuesSpec) [("k1",10::Int), ("k2",20)]+    [["{k1: 10, k2: 20}"]+    ,["{k1: 10, k2: 20,}"]+    ,["k1 : 10"+     ,"k2: 20"]+    ]++  , test valuesSpec [ Left (1::Int), Right ("two"::Text) ]+    [["[1, \"two\"]"]+    ,["* 1"+     ,"* \"two\""]+    ]++  , test (sectionsSpec "test"+            (liftA2 (,) (reqSection "k1" "") (reqSection "k2" "")))+         (10 :: Int, 20 :: Int)+    [["k1: 10"+     ,"k2: 20"]+    ,["k2: 20"+     ,"k1: 10"]+    ]++  , test (sectionsSpec "test"+            (liftA2 (,) (optSection "k1" "") (reqSection "k2" "")))+         (Just 10 :: Maybe Int, 20 :: Int)+    [["k1: 10"+     ,"k2: 20"]+    ,["k2: 20"+     ,"k1: 10"]+    ]++  , test (sectionsSpec "test"+            (liftA2 (,) (optSection "k1" "") (reqSection "k2" "")))+         (Nothing :: Maybe Int, 20 :: Int)+    [["k2: 20"]+    ,["{k2: 20}"]+    ]++  -- This isn't a good idea, but it currently works+  , test (sectionsSpec "test"+            (liftA2 (,) (reqSection "k1" "") (reqSection "k1" "")))+         ("first"::Text, 50::Int)+    [["k1: \"first\""+     ,"k1: 50"]+    ]++  , test (sectionsSpec "test" (pure ())) ()+    [["{}"]+    ]+  ]