packages feed

config-schema 0.5.0.1 → 1.0.0.0

raw patch · 9 files changed

+765/−450 lines, 9 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Config.Schema.Load: LoadError :: p -> [Text] -> Problem -> LoadError p
- Config.Schema.Load: SchemaError :: NonEmpty (LoadError Position) -> SchemaError
- Config.Schema.Load: SpecMismatch :: Text -> Problem
- Config.Schema.Load: UnusedSection :: Text -> Problem
- Config.Schema.Load: data LoadError p
- Config.Schema.Load: instance Data.Functor.Alt.Alt (Config.Schema.Load.Load p)
- Config.Schema.Load: instance GHC.Base.Applicative (Config.Schema.Load.Load p)
- Config.Schema.Load: instance GHC.Base.Functor (Config.Schema.Load.Load p)
- Config.Schema.Load: instance GHC.Base.Monad (Config.Schema.Load.Load p)
- Config.Schema.Load: instance GHC.Classes.Eq Config.Schema.Load.Problem
- Config.Schema.Load: instance GHC.Classes.Ord Config.Schema.Load.Problem
- Config.Schema.Load: instance GHC.Exception.Type.Exception Config.Schema.Load.SchemaError
- Config.Schema.Load: instance GHC.Read.Read Config.Schema.Load.Problem
- Config.Schema.Load: instance GHC.Read.Read p => GHC.Read.Read (Config.Schema.Load.LoadError p)
- Config.Schema.Load: instance GHC.Show.Show Config.Schema.Load.Problem
- Config.Schema.Load: instance GHC.Show.Show Config.Schema.Load.SchemaError
- Config.Schema.Load: instance GHC.Show.Show p => GHC.Show.Show (Config.Schema.Load.LoadError p)
- Config.Schema.Load: newtype SchemaError
- Config.Schema.Spec: [AnyAtomSpec] :: ValueSpec Text
- Config.Schema.Spec: [AssocSpec] :: ValueSpecs a -> ValueSpec [(Text, a)]
- Config.Schema.Spec: [AtomSpec] :: Text -> ValueSpec ()
- Config.Schema.Spec: [CustomSpec] :: Text -> ValueSpecs (Maybe a) -> ValueSpec a
- Config.Schema.Spec: [IntegerSpec] :: ValueSpec Integer
- Config.Schema.Spec: [ListSpec] :: ValueSpecs a -> ValueSpec [a]
- Config.Schema.Spec: [NamedSpec] :: Text -> ValueSpecs a -> ValueSpec a
- Config.Schema.Spec: [OptSection] :: Text -> Text -> ValueSpecs a -> SectionSpec (Maybe a)
- Config.Schema.Spec: [RationalSpec] :: ValueSpec Rational
- Config.Schema.Spec: [ReqSection] :: Text -> Text -> ValueSpecs a -> SectionSpec a
- Config.Schema.Spec: [SectionSpecs] :: Text -> SectionSpecs a -> ValueSpec a
- Config.Schema.Spec: [TextSpec] :: ValueSpec Text
- Config.Schema.Spec: class Spec a
- Config.Schema.Spec: data SectionSpec :: * -> *
- Config.Schema.Spec: data SectionSpecs a
- Config.Schema.Spec: data ValueSpecs a
- Config.Schema.Spec: instance (Config.Schema.Spec.Spec a, Config.Schema.Spec.Spec b) => Config.Schema.Spec.Spec (Data.Either.Either a b)
- Config.Schema.Spec: instance Config.Schema.Spec.Spec Data.Text.Internal.Text
- Config.Schema.Spec: instance Config.Schema.Spec.Spec GHC.Int.Int16
- Config.Schema.Spec: instance Config.Schema.Spec.Spec GHC.Int.Int32
- Config.Schema.Spec: instance Config.Schema.Spec.Spec GHC.Int.Int64
- Config.Schema.Spec: instance Config.Schema.Spec.Spec GHC.Int.Int8
- Config.Schema.Spec: instance Config.Schema.Spec.Spec GHC.Integer.Type.Integer
- Config.Schema.Spec: instance Config.Schema.Spec.Spec GHC.Real.Rational
- Config.Schema.Spec: instance Config.Schema.Spec.Spec GHC.Types.Int
- Config.Schema.Spec: instance Config.Schema.Spec.Spec GHC.Types.Word
- Config.Schema.Spec: instance Config.Schema.Spec.Spec GHC.Word.Word16
- Config.Schema.Spec: instance Config.Schema.Spec.Spec GHC.Word.Word32
- Config.Schema.Spec: instance Config.Schema.Spec.Spec GHC.Word.Word64
- Config.Schema.Spec: instance Config.Schema.Spec.Spec GHC.Word.Word8
- Config.Schema.Spec: instance Config.Schema.Spec.Spec a => Config.Schema.Spec.Spec [a]
- Config.Schema.Spec: instance Data.Functor.Alt.Alt Config.Schema.Spec.ValueSpecs
- Config.Schema.Spec: instance GHC.Base.Applicative Config.Schema.Spec.SectionSpecs
- Config.Schema.Spec: instance GHC.Base.Functor Config.Schema.Spec.SectionSpecs
- Config.Schema.Spec: instance GHC.Base.Functor Config.Schema.Spec.ValueSpecs
- Config.Schema.Spec: liftSectionSpec :: SectionSpec a -> SectionSpecs a
- Config.Schema.Spec: liftValueSpec :: ValueSpec a -> ValueSpecs a
- Config.Schema.Spec: runSections :: Applicative f => (forall x. SectionSpec x -> f x) -> SectionSpecs a -> f a
- Config.Schema.Spec: runSections_ :: Monoid m => (forall x. SectionSpec x -> m) -> SectionSpecs a -> m
- Config.Schema.Spec: runValueSpecs :: Alt f => (forall x. ValueSpec x -> f x) -> ValueSpecs a -> f a
- Config.Schema.Spec: runValueSpecs_ :: Semigroup m => (forall x. ValueSpec x -> m) -> ValueSpecs a -> m
- Config.Schema.Spec: valuesSpec :: Spec a => ValueSpecs a
+ Config.Schema.Load: CustomProblem :: Text -> Problem p
+ Config.Schema.Load: ListElementProblem :: Int -> ValueSpecMismatch p -> Problem p
+ Config.Schema.Load: NestedProblem :: ValueSpecMismatch p -> Problem p
+ Config.Schema.Load: PrimMismatch :: Text -> Problem p -> PrimMismatch p
+ Config.Schema.Load: SubkeyProblem :: Text -> ValueSpecMismatch p -> Problem p
+ Config.Schema.Load: TypeMismatch :: Problem p
+ Config.Schema.Load: UnusedSections :: NonEmpty Text -> Problem p
+ Config.Schema.Load: ValueSpecMismatch :: Value p -> NonEmpty (PrimMismatch p) -> ValueSpecMismatch p
+ Config.Schema.Load: WrongAtom :: Problem p
+ Config.Schema.Load: data PrimMismatch p
+ Config.Schema.Load: data ValueSpecMismatch p
+ Config.Schema.Load.Error: CustomProblem :: Text -> Problem p
+ Config.Schema.Load.Error: ListElementProblem :: Int -> ValueSpecMismatch p -> Problem p
+ Config.Schema.Load.Error: MissingSection :: Text -> Problem p
+ Config.Schema.Load.Error: NestedProblem :: ValueSpecMismatch p -> Problem p
+ Config.Schema.Load.Error: PrimMismatch :: Text -> Problem p -> PrimMismatch p
+ Config.Schema.Load.Error: SubkeyProblem :: Text -> ValueSpecMismatch p -> Problem p
+ Config.Schema.Load.Error: TypeMismatch :: Problem p
+ Config.Schema.Load.Error: UnusedSections :: NonEmpty Text -> Problem p
+ Config.Schema.Load.Error: ValueSpecMismatch :: Value p -> NonEmpty (PrimMismatch p) -> ValueSpecMismatch p
+ Config.Schema.Load.Error: WrongAtom :: Problem p
+ Config.Schema.Load.Error: class (Typeable a, Show a) => ErrorAnnotation a
+ Config.Schema.Load.Error: data PrimMismatch p
+ Config.Schema.Load.Error: data Problem p
+ Config.Schema.Load.Error: data ValueSpecMismatch p
+ Config.Schema.Load.Error: describeSpec :: PrimValueSpec a -> Text
+ Config.Schema.Load.Error: describeValue :: Value p -> String
+ Config.Schema.Load.Error: displayAnnotation :: ErrorAnnotation a => a -> Doc
+ Config.Schema.Load.Error: instance Config.Schema.Load.Error.ErrorAnnotation ()
+ Config.Schema.Load.Error: instance Config.Schema.Load.Error.ErrorAnnotation Config.Tokens.Position
+ Config.Schema.Load.Error: instance Config.Schema.Load.Error.ErrorAnnotation p => GHC.Exception.Type.Exception (Config.Schema.Load.Error.ValueSpecMismatch p)
+ Config.Schema.Load.Error: instance GHC.Show.Show p => GHC.Show.Show (Config.Schema.Load.Error.PrimMismatch p)
+ Config.Schema.Load.Error: instance GHC.Show.Show p => GHC.Show.Show (Config.Schema.Load.Error.Problem p)
+ Config.Schema.Load.Error: instance GHC.Show.Show p => GHC.Show.Show (Config.Schema.Load.Error.ValueSpecMismatch p)
+ Config.Schema.Load.Error: prettyPrimMismatch :: ErrorAnnotation p => PrimMismatch p -> Doc
+ Config.Schema.Load.Error: prettyProblem :: ErrorAnnotation p => Problem p -> (Doc, Doc)
+ Config.Schema.Load.Error: prettyValueSpecMismatch :: ErrorAnnotation p => ValueSpecMismatch p -> Doc
+ Config.Schema.Spec: anySpec :: HasSpec a => ValueSpec a
+ Config.Schema.Spec: class HasSpec a
+ Config.Schema.Spec: data SectionsSpec a
+ Config.Schema.Spec: instance (Config.Schema.Spec.HasSpec a, Config.Schema.Spec.HasSpec b) => Config.Schema.Spec.HasSpec (Data.Either.Either a b)
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec Data.Text.Internal.Text
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec GHC.Int.Int16
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec GHC.Int.Int32
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec GHC.Int.Int64
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec GHC.Int.Int8
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec GHC.Integer.Type.Integer
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec GHC.Real.Rational
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec GHC.Types.Int
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec GHC.Types.Word
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec GHC.Word.Word16
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec GHC.Word.Word32
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec GHC.Word.Word64
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec GHC.Word.Word8
+ Config.Schema.Spec: instance Config.Schema.Spec.HasSpec a => Config.Schema.Spec.HasSpec [a]
+ Config.Schema.Types: [AnyAtomSpec] :: PrimValueSpec Text
+ Config.Schema.Types: [AssocSpec] :: ValueSpec a -> PrimValueSpec [(Text, a)]
+ Config.Schema.Types: [AtomSpec] :: Text -> PrimValueSpec ()
+ Config.Schema.Types: [CustomSpec] :: Text -> ValueSpec (Either Text a) -> PrimValueSpec a
+ Config.Schema.Types: [IntegerSpec] :: PrimValueSpec Integer
+ Config.Schema.Types: [ListSpec] :: ValueSpec a -> PrimValueSpec [a]
+ Config.Schema.Types: [NamedSpec] :: Text -> ValueSpec a -> PrimValueSpec a
+ Config.Schema.Types: [OptSection] :: Text -> Text -> ValueSpec a -> PrimSectionSpec (Maybe a)
+ Config.Schema.Types: [RationalSpec] :: PrimValueSpec Rational
+ Config.Schema.Types: [ReqSection] :: Text -> Text -> ValueSpec a -> PrimSectionSpec a
+ Config.Schema.Types: [SectionsSpec] :: Text -> SectionsSpec a -> PrimValueSpec a
+ Config.Schema.Types: [TextSpec] :: PrimValueSpec Text
+ Config.Schema.Types: data PrimSectionSpec :: * -> *
+ Config.Schema.Types: data PrimValueSpec :: * -> *
+ Config.Schema.Types: data SectionsSpec a
+ Config.Schema.Types: data ValueSpec a
+ Config.Schema.Types: instance Data.Functor.Alt.Alt Config.Schema.Types.ValueSpec
+ Config.Schema.Types: instance GHC.Base.Applicative Config.Schema.Types.SectionsSpec
+ Config.Schema.Types: instance GHC.Base.Functor Config.Schema.Types.SectionsSpec
+ Config.Schema.Types: instance GHC.Base.Functor Config.Schema.Types.ValueSpec
+ Config.Schema.Types: primSectionsSpec :: PrimSectionSpec a -> SectionsSpec a
+ Config.Schema.Types: primValueSpec :: PrimValueSpec a -> ValueSpec a
+ Config.Schema.Types: runSections :: Applicative f => (forall x. PrimSectionSpec x -> f x) -> SectionsSpec a -> f a
+ Config.Schema.Types: runSections_ :: Monoid m => (forall x. PrimSectionSpec x -> m) -> SectionsSpec a -> m
+ Config.Schema.Types: runValueSpec :: Alt f => (forall x. PrimValueSpec x -> f x) -> ValueSpec a -> f a
+ Config.Schema.Types: runValueSpec_ :: Semigroup m => (forall x. PrimValueSpec x -> m) -> ValueSpec a -> m
- Config.Schema.Docs: generateDocs :: ValueSpecs a -> Doc
+ Config.Schema.Docs: generateDocs :: ValueSpec a -> Doc
- Config.Schema.Load: MissingSection :: Text -> Problem
+ Config.Schema.Load: MissingSection :: Text -> Problem p
- Config.Schema.Load: data Problem
+ Config.Schema.Load: data Problem p
- Config.Schema.Load: loadValue :: ValueSpecs a -> Value p -> Either (NonEmpty (LoadError p)) a
+ Config.Schema.Load: loadValue :: ValueSpec a -> Value p -> Either (ValueSpecMismatch p) a
- Config.Schema.Load: loadValueFromFile :: ValueSpecs a -> FilePath -> IO a
+ Config.Schema.Load: loadValueFromFile :: ValueSpec a -> FilePath -> IO a
- Config.Schema.Spec: anyAtomSpec :: ValueSpecs Text
+ Config.Schema.Spec: anyAtomSpec :: ValueSpec Text
- Config.Schema.Spec: assocSpec :: ValueSpecs a -> ValueSpecs [(Text, a)]
+ Config.Schema.Spec: assocSpec :: ValueSpec a -> ValueSpec [(Text, a)]
- Config.Schema.Spec: atomSpec :: Text -> ValueSpecs ()
+ Config.Schema.Spec: atomSpec :: Text -> ValueSpec ()
- Config.Schema.Spec: customSpec :: Text -> ValueSpecs a -> (a -> Maybe b) -> ValueSpecs b
+ Config.Schema.Spec: customSpec :: Text -> ValueSpec a -> (a -> Either Text b) -> ValueSpec b
- Config.Schema.Spec: data ValueSpec :: * -> *
+ Config.Schema.Spec: data ValueSpec a
- Config.Schema.Spec: fractionalSpec :: Fractional a => ValueSpecs a
+ Config.Schema.Spec: fractionalSpec :: Fractional a => ValueSpec a
- Config.Schema.Spec: listSpec :: ValueSpecs a -> ValueSpecs [a]
+ Config.Schema.Spec: listSpec :: ValueSpec a -> ValueSpec [a]
- Config.Schema.Spec: namedSpec :: Text -> ValueSpecs a -> ValueSpecs a
+ Config.Schema.Spec: namedSpec :: Text -> ValueSpec a -> ValueSpec a
- Config.Schema.Spec: nonemptySpec :: ValueSpecs a -> ValueSpecs (NonEmpty a)
+ Config.Schema.Spec: nonemptySpec :: ValueSpec a -> ValueSpec (NonEmpty a)
- Config.Schema.Spec: numSpec :: Num a => ValueSpecs a
+ Config.Schema.Spec: numSpec :: Num a => ValueSpec a
- Config.Schema.Spec: oneOrList :: ValueSpecs a -> ValueSpecs [a]
+ Config.Schema.Spec: oneOrList :: ValueSpec a -> ValueSpec [a]
- Config.Schema.Spec: oneOrNonemptySpec :: ValueSpecs a -> ValueSpecs (NonEmpty a)
+ Config.Schema.Spec: oneOrNonemptySpec :: ValueSpec a -> ValueSpec (NonEmpty a)
- Config.Schema.Spec: optSection :: Spec a => Text -> Text -> SectionSpecs (Maybe a)
+ Config.Schema.Spec: optSection :: HasSpec a => Text -> Text -> SectionsSpec (Maybe a)
- Config.Schema.Spec: optSection' :: Text -> ValueSpecs a -> Text -> SectionSpecs (Maybe a)
+ Config.Schema.Spec: optSection' :: Text -> ValueSpec a -> Text -> SectionsSpec (Maybe a)
- Config.Schema.Spec: reqSection :: Spec a => Text -> Text -> SectionSpecs a
+ Config.Schema.Spec: reqSection :: HasSpec a => Text -> Text -> SectionsSpec a
- Config.Schema.Spec: reqSection' :: Text -> ValueSpecs a -> Text -> SectionSpecs a
+ Config.Schema.Spec: reqSection' :: Text -> ValueSpec a -> Text -> SectionsSpec a
- Config.Schema.Spec: sectionsSpec :: Text -> SectionSpecs a -> ValueSpecs a
+ Config.Schema.Spec: sectionsSpec :: Text -> SectionsSpec a -> ValueSpec a
- Config.Schema.Spec: stringSpec :: ValueSpecs String
+ Config.Schema.Spec: stringSpec :: ValueSpec String
- Config.Schema.Spec: yesOrNoSpec :: ValueSpecs Bool
+ Config.Schema.Spec: yesOrNoSpec :: ValueSpec Bool

Files

ChangeLog.md view
@@ -1,5 +1,19 @@ # Revision history for config-schema +## 1.0.0.0++* Rename `ValueSpec` to `PrimValueSpec`+* Rename `ValueSpecs` to `ValueSpec`+* Rename `SectionSpec` to `PrimSectionSpec`+* Rename `SectionSpecs` to `SectionsSpec`+* Rename `Spec` class to `HasSpec`+* Rename `valuesSpec` to `anySpec`+* Custom specifications changed type to expose an error message.+* Move spec types to `Config.Schema.Types`. Now `Config.Schema.Spec`+  has only the exports needed for building specs and not defining+  new spec consumers.+* Improve schema mismatch type and errors in `Config.Schema.Load.Error`+ ## 0.5.0.1 * Support GHC 8.4.1 
README.md view
@@ -44,7 +44,7 @@ exampleValue :: Value Position Right exampleValue = parse exampleFile -exampleSpec :: ValueSpecs Text+exampleSpec :: ValueSpec Text exampleSpec = sectionsSpec "" $   do name  <- reqSection  "name" "Full name"      age   <- reqSection  "age"  "Age of user"@@ -63,12 +63,12 @@              Text.intercalate ", " kids <>              happyText -kidSpec :: ValueSpecs Text+kidSpec :: ValueSpec Text kidSpec = sectionsSpec "kid" (reqSection "name" "Kid's name")   -- | Matches the 'yes' and 'no' atoms-yesOrNo :: ValueSpecs Bool+yesOrNo :: ValueSpec Bool yesOrNo = True  <$ atomSpec "yes" <!>           False <$ atomSpec "no" 
config-schema.cabal view
@@ -1,5 +1,6 @@+cabal-version:       2.2 name:                config-schema-version:             0.5.0.1+version:             1.0.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.@@ -14,31 +15,38 @@ category:            Language build-type:          Simple extra-source-files:  ChangeLog.md README.md-cabal-version:       >=1.10 homepage:            https://github.com/glguy/config-schema bug-reports:         https://github.com/glguy/config-schema/issues-tested-with:         GHC==7.10.3, GHC==8.0.2+tested-with:         GHC==7.10.3, GHC==8.0.2, GHC==8.2.2, GHC==8.4.4, GHC==8.6.5  source-repository head   type:                 git   location:             https://github.com/glguy/config-schema  library-  exposed-modules:      Config.Schema, Config.Schema.Docs, Config.Schema.Load, Config.Schema.Spec-  build-depends:        base           >=4.8   && <4.12,-                        config-value   >=0.6   && <0.7,-                        containers     >=0.5   && <0.6,-                        free           >=4.12  && <5.1,-                        kan-extensions >=5.0.2 && <5.2,-                        pretty         >=1.1.2 && <1.2,-                        semigroupoids  >=5.1   && <5.3,-                        text           >=1.2   && <1.3,-                        transformers   >=0.4   && <0.6+  exposed-modules:+    Config.Schema+    Config.Schema.Docs+    Config.Schema.Load+    Config.Schema.Load.Error+    Config.Schema.Spec+    Config.Schema.Types +  build-depends:+    base           >=4.8   && <4.13,+    config-value   >=0.6   && <0.7,+    containers     >=0.5   && <0.7,+    free           >=4.12  && <5.2,+    kan-extensions >=5.0.2 && <5.3,+    pretty         >=1.1.2 && <1.2,+    semigroupoids  >=5.1   && <5.4,+    text           >=1.2   && <1.3,+    transformers   >=0.4   && <0.6,+   if flag(use-semigroups)-        build-depends: base <4.9, semigroups >=0.18 && <0.19+    build-depends: base <4.9, semigroups >=0.18 && <0.19   else-        build-depends: base >= 4.9+    build-depends: base >= 4.9    hs-source-dirs:       src   default-language:     Haskell2010
src/Config/Schema/Docs.hs view
@@ -16,7 +16,7 @@ to be able to generate another form are exported by "Config.Schema.Spec".  @-configSpec :: ValueSpecs (Text,Maybe Int)+configSpec :: ValueSpec (Text,Maybe Int) configSpec = sectionsSpec ""            $ liftA2 (,)                (reqSection "username" "Name used to login")@@ -52,9 +52,10 @@ import           Prelude hiding ((<>))  import           Config.Schema.Spec+import           Config.Schema.Types  -- | Default documentation generator.-generateDocs :: ValueSpecs a -> Doc+generateDocs :: ValueSpec a -> Doc generateDocs spec = vcat' docLines   where     sectionLines :: (Text, Doc) -> [Doc]@@ -63,9 +64,9 @@     (topDoc, topMap) = runDocBuilder (valuesDoc False spec)      docLines =-      case runValueSpecs_ (pure . SomeSpec) spec of+      case runValueSpec_ (pure . SomeSpec) spec of         -- single, top-level sections spec-        SomeSpec (SectionSpecs name _) :| []+        SomeSpec (SectionsSpec name _) :| []           | Just top <- Map.lookup name topMap ->               txt "Top-level configuration file fields:" :               nest 4 top :@@ -78,17 +79,17 @@   -- | Forget the type of the value spec-data SomeSpec where SomeSpec :: ValueSpec a -> SomeSpec+data SomeSpec where SomeSpec :: PrimValueSpec a -> SomeSpec   -- | 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 Doc+sectionsDoc :: Text -> SectionsSpec 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 Doc+sectionDoc :: PrimSectionSpec a -> DocBuilder Doc sectionDoc s =   case s of     ReqSection name desc w -> aux "REQUIRED" name desc <$> valuesDoc False w@@ -107,9 +108,9 @@ -- -- Set nested to 'True' when using valuesDoc in a nested context and -- parentheses would be needed in the case of multiple alternatives.-valuesDoc :: Bool {- ^ nested -} -> ValueSpecs a -> DocBuilder Doc+valuesDoc :: Bool {- ^ nested -} -> ValueSpec a -> DocBuilder Doc valuesDoc nested =-  fmap (disjunction nested) . sequenceA . runValueSpecs_ (fmap pure valueDoc)+  fmap (disjunction nested) . sequenceA . runValueSpec_ (fmap pure valueDoc)   -- | Combine a list of text with the word @or@.@@ -121,7 +122,7 @@   -- | Compute the documentation fragment for an individual value specification.-valueDoc :: ValueSpec a -> DocBuilder Doc+valueDoc :: PrimValueSpec a -> DocBuilder Doc valueDoc w =   case w of     TextSpec         -> pure "text"@@ -129,7 +130,7 @@     RationalSpec     -> pure "number"     AtomSpec a       -> pure ("`" <> txt a <> "`")     AnyAtomSpec      -> pure "atom"-    SectionSpecs l s -> sectionsDoc l s+    SectionsSpec l s -> sectionsDoc l s     NamedSpec    l s -> emitDoc l (valuesDoc False s)     CustomSpec l w'  -> (txt l                 <+>) <$> valuesDoc True w'     ListSpec ws      -> ("list of"             <+>) <$> valuesDoc True ws
src/Config/Schema/Load.hs view
@@ -1,4 +1,4 @@-{-# Language OverloadedStrings, GeneralizedNewtypeDeriving, GADTs #-}+{-# Language GADTs #-} {-| Module      : Config.Schema.Load Description : Operations to extract a value from a configuration.@@ -15,166 +15,137 @@   , loadValueFromFile    -- * Errors-  , SchemaError(..)-  , LoadError(..)+  , ValueSpecMismatch(..)+  , PrimMismatch(..)   , Problem(..)   ) where -import           Control.Exception                (Exception(..), throwIO)+import           Control.Exception                (throwIO) 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           Control.Monad.Trans.State        (StateT(..), runStateT, state)+import           Control.Monad.Trans.Except       (Except, runExcept, throwE, withExcept) import           Data.Ratio                       (numerator, denominator) import           Data.List.NonEmpty               (NonEmpty) import qualified Data.List.NonEmpty as NonEmpty import           Data.Text                        (Text)-import qualified Data.Text as Text import qualified Data.Text.IO as Text  import           Config-import           Config.Schema.Spec+import           Config.Schema.Types+import           Config.Schema.Load.Error  --- | Match a 'Value' against a 'ValueSpecs' and return either+-- | Match a 'Value' against a 'ValueSpec' and return either -- the interpretation of that value or the list of errors -- encountered. loadValue ::-  ValueSpecs a                      {- ^ specification           -} ->+  ValueSpec a                       {- ^ specification           -} ->   Value p                           {- ^ value                   -} ->-  Either (NonEmpty (LoadError p)) a {- ^ errors or decoded value -}-loadValue spec val = runLoad (getValue spec val)+  Either (ValueSpecMismatch p) a {- ^ errors or decoded value -}+loadValue spec val = runExcept (getValue spec val)   -- | Read a configuration file, parse it, and validate it according -- to the given specification. ----- Throws 'IOError', 'ParseError', or 'SchemaError'+-- Throws 'IOError', 'ParseError', or 'ValueSpecMismatch' loadValueFromFile ::-  ValueSpecs a {- ^ specification -} ->-  FilePath     {- ^ filename      -} ->+  ValueSpec a {- ^ specification -} ->+  FilePath    {- ^ filename      -} ->   IO a loadValueFromFile spec path =   do txt <- Text.readFile path-     val <- either throwIO return (parse txt)-     either (throwIO . SchemaError) return (loadValue spec val)----- | Newtype wrapper for schema load errors.-newtype SchemaError = SchemaError (NonEmpty (LoadError Position))-  deriving Show---- | Custom 'displayException' implementation-instance Exception SchemaError where-  displayException (SchemaError e) = foldr showLoadError "" e-    where-      showLoadError (LoadError pos path problem)-        = shows (posLine pos)-        . showChar ':'-        . shows (posColumn pos)-        . showString ": "-        . foldr (\x xs -> showString (Text.unpack x) . showChar ':' . xs) id path-        . showChar ' '-        . showProblem problem-        . showChar '\n'--      showProblem p =-        case p of-          MissingSection x -> showString "missing required section `"-                            . showString (Text.unpack x) . showChar '`'-          UnusedSection  x -> showString "unused section `"-                            . showString (Text.unpack x) . showChar '`'-          SpecMismatch   x -> showString "expected " . showString (Text.unpack x)-+     let exceptIO m = either throwIO return m+     val <- exceptIO (parse txt)+     exceptIO (loadValue spec val) -getSection :: p -> SectionSpec a -> StateT [Section p] (Load p) a-getSection pos (ReqSection k _ w) =-  do v <- StateT (lookupSection pos k)-     lift (scope k (getValue w v))-getSection pos (OptSection k _ w) =-  do mb <- optional1 (StateT (lookupSection pos k))-     lift (traverse (scope k . getValue w) mb)+getSection :: PrimSectionSpec a -> StateT [Section p] (Except (Problem p)) a+getSection (ReqSection k _ w) =+  do mb <- state (lookupSection k)+     lift $ case mb of+       Just v -> getValue' (SubkeyProblem k) w v+       Nothing -> throwE (MissingSection k)+getSection (OptSection k _ w) =+  do mb <- state (lookupSection k)+     lift (traverse (getValue' (SubkeyProblem k) w) mb)  -getSections :: p -> SectionSpecs a -> [Section p] -> Load p a-getSections pos spec xs =-  do (a,leftovers) <- runStateT (runSections (getSection pos) spec) xs+getSections :: SectionsSpec a -> [Section p] -> Except (Problem p) a+getSections spec xs =+  do (a,leftovers) <- runStateT (runSections getSection spec) xs      case NonEmpty.nonEmpty leftovers of        Nothing -> return a-       Just ss -> asum1 (fmap (\s -> loadFail (sectionAnn s) (UnusedSection (sectionName s))) ss)+       Just ss -> throwE (UnusedSections (fmap sectionName ss))  -getValue :: ValueSpecs a -> Value p -> Load p a-getValue s v = runValueSpecs (getValue1 v) s+getValue :: ValueSpec a -> Value p -> Except (ValueSpecMismatch p) a+getValue s v = withExcept (ValueSpecMismatch v) (runValueSpec (getValue1 v) s) +-- | Match a 'Value' against a 'ValueSpec' given a wrapper for any nested+-- mismatch errors that might occur.+getValue' ::+  (ValueSpecMismatch p -> Problem p) ->+  ValueSpec a ->+  Value p ->+  Except (Problem p) a+getValue' p s v = withExcept (p . ValueSpecMismatch v) (runValueSpec (getValue1 v) s) --- | Match a primitive value specification against a single value.-getValue1 :: Value p -> ValueSpec a -> Load p 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 :: Value p -> PrimValueSpec a -> Except (NonEmpty (PrimMismatch p)) a+getValue1 v prim = withExcept (pure . PrimMismatch (describeSpec prim))+                              (getValue2 v prim) -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")+-- | Match a primitive value specification against a single value.+getValue2 :: Value p -> PrimValueSpec a -> Except (Problem p) a+getValue2 (Text _ t)       TextSpec           = pure t+getValue2 (Number _ _ n)   IntegerSpec        = pure n+getValue2 (Floating _ a b) IntegerSpec | Just i <- floatingToInteger a b = pure i+getValue2 (Number _ _ n)   RationalSpec       = pure (fromInteger n)+getValue2 (Floating _ a b) RationalSpec       = pure (floatingToRational a b)+getValue2 (List _ xs)      (ListSpec w)       = getList w xs+getValue2 (Atom _ b)       AnyAtomSpec        = pure (atomName b)+getValue2 (Atom _ b)       (AtomSpec a)+  | a == atomName b = pure ()+  | otherwise       = throwE WrongAtom+getValue2 (Sections _ s)   (SectionsSpec _ w) = getSections w s+getValue2 (Sections _ s)   (AssocSpec w)      = getAssoc w s+getValue2 v                (NamedSpec _ w)    = getValue' NestedProblem w v+getValue2 v                (CustomSpec _ w)   = getCustom w v+getValue2 _                _                  = throwE TypeMismatch   -- | 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 p] -> Load p [a]-getList w = zipWithM (\i x -> scope (Text.pack (show i)) (getValue w x)) [1::Int ..]+getList :: ValueSpec a -> [Value p] -> Except (Problem p) [a]+getList w = zipWithM (\i -> getValue' (ListElementProblem i) w) [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 p] -> Load p [(Text,a)]-getAssoc w = traverse $ \(Section _ k v) -> (,) k <$> scope k (getValue w v)-+getAssoc :: ValueSpec a -> [Section p] -> Except (Problem p) [(Text,a)]+getAssoc w = traverse $ \(Section _ k v) ->+                 (,) k <$> getValue' (SubkeyProblem k) 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 p              {- ^ value         -} ->-  Load p a-getCustom l w v =-  do x <- getValue w v-     case x of-       Nothing -> loadFail (valueAnn v) (SpecMismatch l)-       Just y  -> pure y+  ValueSpec (Either Text a) {- ^ specification -} ->+  Value p                   {- ^ value         -} ->+  Except (Problem p) a+getCustom w v = either (throwE . CustomProblem) pure =<< getValue' NestedProblem w v   -- | Extract a section from a list of sections by name. lookupSection ::-  p                             {- ^ starting position of sections      -} ->-  Text                          {- ^ section name                       -} ->-  [Section p]                   {- ^ available sections                 -} ->-  Load p (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 pos key xs-                   return (v',s:xs')+  Text                         {- ^ section name                       -} ->+  [Section p]                  {- ^ available sections                 -} ->+  (Maybe (Value p), [Section p]) {- ^ found value and remaining sections -}+lookupSection _ [] = (Nothing, [])+lookupSection key (s@(Section _ k v):xs)+  | key == k  = (Just v, xs)+  | otherwise = case lookupSection key xs of+                  (res, xs') -> (res, s:xs')  ------------------------------------------------------------------------ @@ -189,53 +160,3 @@   | denominator r == 1 = Just (numerator r)   | otherwise          = Nothing   where r = floatingToRational x y----------------------------------------------------------------------------- Error reporting type------------------------------------------------------------------------------ | Type used to match values against specifiations. This type tracks--- the current nested fields (updated with scope) and can throw--- errors using loadFail.-newtype Load p a = MkLoad { unLoad :: ReaderT [Text] (Except (NonEmpty (LoadError p))) a }-  deriving (Functor, Applicative, Monad)--instance Alt (Load p) where MkLoad x <!> MkLoad y = MkLoad (x <!> y)---- | 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 p = LoadError p [Text] Problem -- ^ position, path, problem-  deriving (Read, Show)----- | Run the Load computation until it produces a result or terminates--- with a list of errors.-runLoad :: Load p a -> Either (NonEmpty (LoadError p)) a-runLoad = runExcept . flip runReaderT [] . unLoad----- | Problems that can be encountered when matching a 'Value' against a 'ValueSpecs'.-data Problem-  = 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.-scope :: Text -> Load p a -> Load p a-scope key (MkLoad m) = MkLoad (local (key:) m)---- | Abort value specification matching with the given error.-loadFail :: p -> Problem -> Load p a-loadFail pos cause = MkLoad $-  do path <- ask-     lift (throwE (pure (LoadError pos (reverse path) cause)))------------------------------------------------------------------------------ | One or none. This definition is different from the normal @optional@ definition--- because it uses 'Alt'. This allows it to work on types that are not @Alternative@.-optional1 :: (Applicative f, Alt f) => f a -> f (Maybe a)-optional1 fa = Just <$> fa <!> pure Nothing
+ src/Config/Schema/Load/Error.hs view
@@ -0,0 +1,208 @@+{-# Language GADTs, OverloadedStrings #-}+{-|+Module      : Config.Schema.Load.Error+Description : Error types and rendering for Load module+Copyright   : (c) Eric Mertens, 2019+License     : ISC+Maintainer  : emertens@gmail.com++This module provides a complete skeleton of the failures that+occurred when trying to match a 'Value' against a 'ValueSpec'+allowing custom error rendering to be implemented.++The structure is you get a single value and a list of one-or-more+primitive specifications that it failed to match along with+an enumeration of why that specification failed to match. Some+failures are due to failures in nested specifications, so the+whole error structure can form a tree.++-}+module Config.Schema.Load.Error+  (+  -- * Error types+    ValueSpecMismatch(..)+  , PrimMismatch(..)+  , Problem(..)+  , ErrorAnnotation(..)++  -- * Detailed rendering+  , prettyValueSpecMismatch+  , prettyPrimMismatch+  , prettyProblem++  -- * Summaries+  , describeSpec+  , describeValue+  ) where++import           Control.Exception+import           Data.Text (Text)+import           Data.Foldable (toList)+import qualified Data.Text as Text+import           Data.List.NonEmpty (NonEmpty((:|)))+import qualified Data.List.NonEmpty as NonEmpty+import           Data.Typeable (Typeable)+import           Text.PrettyPrint+                    (Doc, fsep, ($+$), nest, text, vcat, (<+>), empty,+                     punctuate, comma, int, colon, hcat)+import           Data.Monoid ((<>))++import           Config+import           Config.Schema.Types++-- | Newtype wrapper for schema load errors.+data ValueSpecMismatch p =+  -- | Problem value and list of specification failures+  ValueSpecMismatch (Value p) (NonEmpty (PrimMismatch p))+  deriving Show++-- | 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 PrimMismatch p =+  -- | spec description and problem+  PrimMismatch Text (Problem p)+  deriving Show+++-- | Problems that can be encountered when matching a 'Value' against a 'ValueSpec'.+data Problem p+  = MissingSection Text                          -- ^ missing section name+  | UnusedSections (NonEmpty Text)               -- ^ unused section names+  | SubkeyProblem Text     (ValueSpecMismatch p) -- ^ nested error in given section+  | ListElementProblem Int (ValueSpecMismatch p) -- ^ nested error in given list element+  | NestedProblem          (ValueSpecMismatch p) -- ^ generic nested error+  | TypeMismatch                                 -- ^ value and spec type mismatch+  | CustomProblem Text                           -- ^ custom spec error message+  | WrongAtom                                    -- ^ atoms didn't match+  deriving Show++-- | Describe outermost shape of a 'PrimValueSpec'+describeSpec :: PrimValueSpec a -> Text+describeSpec TextSpec                   = "text"+describeSpec IntegerSpec                = "integer"+describeSpec RationalSpec               = "number"+describeSpec AnyAtomSpec                = "atom"+describeSpec (AtomSpec a)               = "atom `" <> a <> "`"+describeSpec (ListSpec _)               = "list"+describeSpec (SectionsSpec name _)      = name+describeSpec (AssocSpec _)              = "sections"+describeSpec (CustomSpec name _)        = name+describeSpec (NamedSpec name _)         = name++-- | Describe outermost shape of a 'Value'+describeValue :: Value p -> String+describeValue Text{}     = "text"+describeValue Number{}   = "integer"+describeValue Floating{} = "number"+describeValue (Atom _ a) = "atom `" <> Text.unpack (atomName a) <> "`"+describeValue Sections{} = "sections"+describeValue List{}     = "list"++-- | Bottom-up transformation of a 'ValueSpecMismatch'+rewriteMismatch ::+  (ValueSpecMismatch p -> ValueSpecMismatch p) ->+  ValueSpecMismatch p -> ValueSpecMismatch p+rewriteMismatch f (ValueSpecMismatch v prims) = f (ValueSpecMismatch v (fmap aux1 prims))+  where+    aux1 (PrimMismatch spec prob) = PrimMismatch spec (aux2 prob)++    aux2 (SubkeyProblem      x y) = SubkeyProblem      x (rewriteMismatch f y)+    aux2 (ListElementProblem x y) = ListElementProblem x (rewriteMismatch f y)+    aux2 (NestedProblem        y) = NestedProblem        (rewriteMismatch f y)+    aux2 prob                     = prob++-- | Single-step rewrite that removes type-mismatch problems if there+-- are non-mismatches available to focus on.+removeTypeMismatch1 :: ValueSpecMismatch p -> ValueSpecMismatch p+removeTypeMismatch1 (ValueSpecMismatch v xs)+  | Just xs' <- NonEmpty.nonEmpty (NonEmpty.filter isNotTypeMismatch xs)+  = ValueSpecMismatch v xs'+  where+    isNotTypeMismatch (PrimMismatch _ TypeMismatch) = False+    isNotTypeMismatch _                             = True+removeTypeMismatch1 v = v++-- | Single-step rewrite that removes mismatches with only a single,+-- nested mismatch below them.+focusMismatch1 :: ValueSpecMismatch p -> ValueSpecMismatch p+focusMismatch1 x@(ValueSpecMismatch _ prims)+  | PrimMismatch _ problem :| [] <- prims+  , Just sub <- simplify1 problem = sub+  | otherwise = x+  where+    simplify1 (SubkeyProblem      _ p) = Just p+    simplify1 (ListElementProblem _ p) = Just p+    simplify1 (NestedProblem        p) = Just p+    simplify1 _                        = Nothing+++-- | Pretty-printer for 'ValueSpecMismatch' showing the position+-- and type of value that failed to match along with details about+-- each specification that it didn't match.+prettyValueSpecMismatch :: ErrorAnnotation p => ValueSpecMismatch p -> Doc+prettyValueSpecMismatch (ValueSpecMismatch v es) =+  heading $+$ errors+  where+    heading = displayAnnotation (valueAnn v) <> text (describeValue v)+    errors = vcat (map prettyPrimMismatch (toList es))+++-- | Pretty-printer for 'PrimMismatch' showing a summary of the primitive+-- specification that didn't match followed by a more detailed error when+-- appropriate.+prettyPrimMismatch :: ErrorAnnotation p => PrimMismatch p -> Doc+prettyPrimMismatch (PrimMismatch spec problem) =+  case prettyProblem problem of+    (summary, detail) ->+      (text "*" <+> text (Text.unpack spec) <+> summary) $+$ nest 4 detail++-- | Pretty-printer for 'Problem' that generates a summary line+-- as well as a detailed description (depending on the error)+prettyProblem ::+  ErrorAnnotation p =>+  Problem p ->+  (Doc, Doc) {- ^ summary, detailed -}+prettyProblem p =+  case p of+    TypeMismatch ->+      ( text "- type mismatch"+      , empty)+    WrongAtom ->+      ( text "- wrong atom"+      , empty)+    MissingSection name ->+      ( text "- missing section:" <+> text (Text.unpack name)+      , empty)+    UnusedSections names ->+      ( text "- unexpected sections:" <+>+        fsep (punctuate comma (map (text . Text.unpack) (toList names)))+      , empty)+    CustomProblem e ->+      ( text "-" <+> text (Text.unpack e)+      , empty)+    SubkeyProblem name e ->+      ( text "- problem in section:" <+> text (Text.unpack name)+      , prettyValueSpecMismatch e)+    NestedProblem e ->+      ( empty+      , prettyValueSpecMismatch e)+    ListElementProblem i e ->+      ( text "- problem in element:" <+> int i+      , prettyValueSpecMismatch e)++-- | Class for rendering position annotations within the 'prettyValueSpecMismatch'+class (Typeable a, Show a) => ErrorAnnotation a where+  displayAnnotation :: a -> Doc++-- | Renders a 'Position' as @line:column:@+instance ErrorAnnotation Position where+  displayAnnotation pos = hcat [int (posLine pos), colon, int (posColumn pos), colon]++-- | Renders as an empty document+instance ErrorAnnotation () where+  displayAnnotation _ = empty++-- | 'displayException' implemented with 'prettyValueSpecMismatch'+instance ErrorAnnotation p => Exception (ValueSpecMismatch p) where+  displayException = show . prettyValueSpecMismatch . rewriteMismatch (focusMismatch1 . removeTypeMismatch1)
src/Config/Schema/Spec.hs view
@@ -1,17 +1,16 @@-{-# Language FlexibleInstances, RankNTypes, GADTs, KindSignatures #-}-{-# Language DeriveFunctor, GeneralizedNewtypeDeriving, OverloadedStrings #-}+{-# Language FlexibleInstances, OverloadedStrings #-}  {-| Module      : Config.Schema.Spec-Description : Types and operations for describing a configuration file format.+Description : Operations for describing a configuration file format. Copyright   : (c) Eric Mertens, 2017 License     : ISC Maintainer  : emertens@gmail.com  This module provides a set of types and operations for defining configuration-file schemas. These schemas can be built up using 'Applicative' operations.+file schemas. -These specifications are suitable for be consumed by "Config.Schema.Load"+These specifications are can be consumed by "Config.Schema.Load" and "Config.Schema.Docs".  This is the schema system used by the @glirc@ IRC client@@ -21,16 +20,9 @@ -} module Config.Schema.Spec   (-  -- * Specifying sections-    SectionSpecs-  , reqSection-  , optSection-  , reqSection'-  , optSection'-   -- * Specifying values-  , ValueSpecs-  , Spec(..)+  -- $values+    ValueSpec   , sectionsSpec   , assocSpec   , atomSpec@@ -38,7 +30,16 @@   , listSpec   , customSpec   , namedSpec+  , HasSpec(..) +  -- * Specifying sections+  -- $sections+  , SectionsSpec+  , reqSection+  , optSection+  , reqSection'+  , optSection'+   -- * Derived specifications   , oneOrList   , yesOrNoSpec@@ -48,269 +49,151 @@   , nonemptySpec   , oneOrNonemptySpec -  -- * Executing specifications-  , runSections-  , runSections_-  , runValueSpecs-  , runValueSpecs_--  -- * Primitive specifications-  , SectionSpec(..)-  , liftSectionSpec-  , ValueSpec(..)-  , liftValueSpec-   ) where -import           Control.Applicative              (Const(..))-import           Control.Applicative.Free         (Ap, runAp, runAp_, liftAp) import           Data.Bits                        (Bits, toIntegralSized)-import           Data.Functor.Coyoneda            (Coyoneda(..), liftCoyoneda, lowerCoyoneda, hoistCoyoneda) import           Data.Functor.Alt                 (Alt(..)) import           Data.Int 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 import           Data.Word +import           Config.Schema.Types+ --------------------------------------------------------------------------- Specifications for sections+-- 'ValueSpec' builders ------------------------------------------------------------------------ --- | Specifications for single configuration sections.+-- $values ----- The fields are section name, documentation text, value specification.--- Use 'ReqSection' for required key-value pairs and 'OptSection' for--- optional ones.-data SectionSpec :: * -> * where--  -- | Required section: Name, Documentation, Specification-  ReqSection :: Text -> Text -> ValueSpecs a -> SectionSpec a--  -- | Optional section: Name, Documentation, Specification-  OptSection :: Text -> Text -> ValueSpecs a -> SectionSpec (Maybe a)----- | A list of section specifications used to process a whole group of--- key-value pairs. Multiple section specifications can be combined--- using this type's 'Applicative' instance.-newtype SectionSpecs a = MkSectionSpecs (Ap SectionSpec a)-  deriving (Functor, Applicative)----- | Lift a single specification into a list of specifications.+-- 'ValueSpec' allows you to define specifications that will match+-- parsed config-value configuration files. 'ValueSpec' allows+-- us to define the shape of configuration values that will match+-- the specification as well as a way to process those matches. ----- @since 0.2.0.0-liftSectionSpec :: SectionSpec a -> SectionSpecs a-liftSectionSpec = MkSectionSpecs . liftAp----- | Given an function that handles a single, primitive section specification;--- 'runSections' will generate one that processes a whole 'SectionsSpec'.+-- Below we have an example configuration record that can be matched+-- from a configuration file. ----- The results from each section will be sequence together using the 'Applicative'--- instance in of the result type, and the results can be indexed by the type--- parameter of the specification.+-- More documentation for defining key-value pairs is available below. ----- For an example use of 'runSections', see "Config.Schema.Load".-runSections :: Applicative f => (forall x. SectionSpec x -> f x) -> SectionSpecs a -> f a-runSections f (MkSectionSpecs s) = runAp f s----- | Given an function that handles a single, primitive section specification;--- 'runSections_' will generate one that processes a whole 'SectionsSpec'.+-- This configuration file expects either a given username or allows+-- the user to ask for a random username. The ('<!>') operator allows+-- us to combine two alternatives as seen below. The config-value+-- language distinguishes between atoms like @random@ and strings like+-- @"random"@ allowing unambiguous special cases to be added in addition+-- to free-form text. ----- The results from each section will be sequence together using the 'Monoid'--- instance in of the result type, and the results will not be indexed by the--- type parameter of the specifications.+-- @+-- {-\# Language RecordWildCards, OverloadedStrings, ApplicativeDo \#-}+-- module Example where ----- For an example use of 'runSections_', see "Config.Schema.Docs".-runSections_ :: Monoid m => (forall x. SectionSpec x -> m) -> SectionSpecs a -> m-runSections_ f (MkSectionSpecs s) = runAp_ f s------------------------------------------------------------------------------ 'SectionSpecs' builders------------------------------------------------------------------------------ | Specification for a required section with an implicit value specification.-reqSection ::-  Spec a =>-  Text {- ^ section name -} ->-  Text {- ^ description  -} ->-  SectionSpecs a-reqSection n i = liftSectionSpec (ReqSection n i valuesSpec)----- | Specification for a required section with an explicit value specification.-reqSection' ::-  Text         {- ^ section name        -} ->-  ValueSpecs a {- ^ value specification -} ->-  Text         {- ^ description         -} ->-  SectionSpecs a-reqSection' n w i = liftSectionSpec (ReqSection n i w)----- | Specification for an optional section with an implicit value specification.-optSection ::-  Spec a =>-  Text {- ^ section name -} ->-  Text {- ^ description  -} ->-  SectionSpecs (Maybe a)-optSection n i = liftSectionSpec (OptSection n i valuesSpec)----- | Specification for an optional section with an explicit value specification.-optSection' ::-  Text         {- ^ section name        -} ->-  ValueSpecs a {- ^ value specification -} ->-  Text         {- ^ description         -} ->-  SectionSpecs (Maybe a)-optSection' n w i = liftSectionSpec (OptSection n i w)------------------------------------------------------------------------------ Specifications for values----------------------------------------------------------------------------- | The primitive specification descriptions for values. Specifications--- built from these primitive cases are found in 'ValueSpecs'.-data ValueSpec :: * -> * where-  -- | Matches any string literal-  TextSpec :: ValueSpec Text--  -- | Matches integral numbers-  IntegerSpec :: ValueSpec Integer--  -- | Matches any number-  RationalSpec :: ValueSpec Rational--  -- | Matches any atom-  AnyAtomSpec :: ValueSpec Text--  -- | Specific atom to be matched-  AtomSpec :: Text -> ValueSpec ()--  -- | Matches a list of the underlying specification-  ListSpec :: ValueSpecs a -> ValueSpec [a]--  -- | 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--  -- | Label used to hide complicated specs in documentation-  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.+-- import "Config.Schema"+-- import "Data.Functor.Alt" (('<!>'))+-- import "Data.Maybe"       ('Data.Maybe.fromMaybe')+-- import "Data.Text"        ('Text') ----- Multiple specifications can be combined using this type's 'Alt' instance.-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. 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. 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'.+-- data Config = Config+--   { userName :: UserName+--   , retries  :: 'Int'+--   } ----- @since 0.2.0.0-liftValueSpec :: ValueSpec a -> ValueSpecs a-liftValueSpec = MkValueSpecs . pure . liftCoyoneda------------------------------------------------------------------------------ 'ValueSpecs' builders--------------------------------------------------------------------------+-- data UserName = Random | Given 'Text'+--+-- userNameSpec :: ValueSpec UserName+-- userNameSpec = Random '<$'  'atomSpec' \"random\"+--            '<!>' Given  '<$>' 'anySpec' -- matches string literals+--+-- nameExample :: 'ValueSpec' Config+-- nameExample = 'sectionsSpec' \"config\" '$'+--+--   do userName <- 'reqSection'' \"username\" userNameSpec \"Configured user name\"+--+--      retries  <- 'Data.Maybe.fromMaybe' 3+--              '<$>' 'optSection' \"retries\" \"Number of attempts (default: 3)\"+--+--      'pure' Config{..}+-- @+--+-- Examples:+--+-- > username: random+-- > retries: 5+-- > -- Generates: Config { userName = Random, retries = 5 }+--+-- We can omit the retries:+--+-- > username: random+-- > -- Generates: Config { userName = Random, retries = 3 }+--+-- We can specify a specific username as a string literal instead+-- of using the atom @random@:+--+-- > username: "me"+-- > -- Generates: Config { userName = Given "me", retries = 3 }+--+-- Sections can be reordered:+--+-- > retries: 5+-- > username: random+-- > -- Generates: Config { userName = Random, retries = 5 }  -- | Class of value specifications that don't require arguments.-class    Spec a       where valuesSpec :: ValueSpecs a-instance Spec Text    where valuesSpec = liftValueSpec TextSpec-instance Spec Integer where valuesSpec = liftValueSpec IntegerSpec-instance Spec Rational where valuesSpec = liftValueSpec RationalSpec-instance Spec a => Spec [a] where valuesSpec = liftValueSpec (ListSpec valuesSpec)-instance (Spec a, Spec b) => Spec (Either a b) where-  valuesSpec = Left <$> valuesSpec <!> Right <$> valuesSpec+class    HasSpec a        where anySpec :: ValueSpec a+instance HasSpec Text     where anySpec = primValueSpec TextSpec+instance HasSpec Integer  where anySpec = primValueSpec IntegerSpec+instance HasSpec Rational where anySpec = primValueSpec RationalSpec+instance HasSpec Int      where anySpec = sizedBitsSpec "machine-bit signed"+instance HasSpec Int8     where anySpec = sizedBitsSpec "8-bit signed"+instance HasSpec Int16    where anySpec = sizedBitsSpec "16-bit signed"+instance HasSpec Int32    where anySpec = sizedBitsSpec "32-bit signed"+instance HasSpec Int64    where anySpec = sizedBitsSpec "64-bit signed"+instance HasSpec Word     where anySpec = sizedBitsSpec "machine-bit unsigned"+instance HasSpec Word8    where anySpec = sizedBitsSpec "8-bit unsigned"+instance HasSpec Word16   where anySpec = sizedBitsSpec "16-bit unsigned"+instance HasSpec Word32   where anySpec = sizedBitsSpec "32-bit unsigned"+instance HasSpec Word64   where anySpec = sizedBitsSpec "64-bit unsigned" -instance Spec Int    where valuesSpec = sizedBitsSpec "machine-bit signed"-instance Spec Int8   where valuesSpec = sizedBitsSpec "8-bit signed"-instance Spec Int16  where valuesSpec = sizedBitsSpec "16-bit signed"-instance Spec Int32  where valuesSpec = sizedBitsSpec "32-bit signed"-instance Spec Int64  where valuesSpec = sizedBitsSpec "64-bit signed"-instance Spec Word   where valuesSpec = sizedBitsSpec "machine-bit unsigned"-instance Spec Word8  where valuesSpec = sizedBitsSpec "8-bit unsigned"-instance Spec Word16 where valuesSpec = sizedBitsSpec "16-bit unsigned"-instance Spec Word32 where valuesSpec = sizedBitsSpec "32-bit unsigned"-instance Spec Word64 where valuesSpec = sizedBitsSpec "64-bit unsigned"+instance HasSpec a => HasSpec [a] where+  anySpec = primValueSpec (ListSpec anySpec) -sizedBitsSpec :: (Integral a, Bits a) => Text -> ValueSpecs a-sizedBitsSpec name = customSpec name (liftValueSpec IntegerSpec) toIntegralSized+instance (HasSpec a, HasSpec b) => HasSpec (Either a b) where+  anySpec = Left <$> anySpec <!> Right <$> anySpec +sizedBitsSpec :: (Integral a, Bits a) => Text -> ValueSpec a+sizedBitsSpec name = customSpec name (primValueSpec IntegerSpec) check+  where+    check i = case toIntegralSized i of+                Nothing -> Left "out of bounds"+                Just j  -> Right j+ -- | Specification for matching a particular atom.-atomSpec :: Text -> ValueSpecs ()-atomSpec = liftValueSpec . AtomSpec+atomSpec :: Text -> ValueSpec ()+atomSpec = primValueSpec . AtomSpec  -- | Specification for matching any atom. Matched atom is returned.-anyAtomSpec :: ValueSpecs Text-anyAtomSpec = liftValueSpec AnyAtomSpec+anyAtomSpec :: ValueSpec Text+anyAtomSpec = primValueSpec AnyAtomSpec  -- | Specification for matching any text as a 'String'-stringSpec :: ValueSpecs String-stringSpec = Text.unpack <$> valuesSpec+stringSpec :: ValueSpec String+stringSpec = Text.unpack <$> anySpec  -- | Specification for matching any integral number.-numSpec :: Num a => ValueSpecs a-numSpec = fromInteger <$> valuesSpec+numSpec :: Num a => ValueSpec a+numSpec = fromInteger <$> anySpec  -- | Specification for matching any fractional number. -- -- @since 0.2.0.0-fractionalSpec :: Fractional a => ValueSpecs a-fractionalSpec = fromRational <$> valuesSpec+fractionalSpec :: Fractional a => ValueSpec a+fractionalSpec = fromRational <$> anySpec  -- | Specification for matching a list of values each satisfying a -- given element specification.-listSpec :: ValueSpecs a -> ValueSpecs [a]-listSpec = liftValueSpec . ListSpec+listSpec :: ValueSpec a -> ValueSpec [a]+listSpec = primValueSpec . ListSpec   -- | Named subsection value specification. The unique identifier will be used@@ -318,9 +201,9 @@ -- be unique within the scope of the specification being built. sectionsSpec ::   Text           {- ^ unique documentation identifier -} ->-  SectionSpecs a {- ^ underlying specification        -} ->-  ValueSpecs a-sectionsSpec i s = liftValueSpec (SectionSpecs i s)+  SectionsSpec a {- ^ underlying specification        -} ->+  ValueSpec a+sectionsSpec i s = primValueSpec (SectionsSpec i s)   -- | Specification for a section list where the keys are user-defined.@@ -329,9 +212,9 @@ -- -- @since 0.3.0.0 assocSpec ::-  ValueSpecs a {- ^ underlying specification -} ->-  ValueSpecs [(Text,a)]-assocSpec = liftValueSpec . AssocSpec+  ValueSpec a {- ^ underlying specification -} ->+  ValueSpec [(Text,a)]+assocSpec = primValueSpec . AssocSpec   -- | Named value specification. This is useful for factoring complicated@@ -339,28 +222,28 @@ -- complex specifications. namedSpec ::   Text         {- ^ name                     -} ->-  ValueSpecs a {- ^ underlying specification -} ->-  ValueSpecs a-namedSpec n s = liftValueSpec (NamedSpec n s)+  ValueSpec a {- ^ underlying specification -} ->+  ValueSpec a+namedSpec n s = primValueSpec (NamedSpec n s)   -- | Specification that matches either a single element or multiple -- elements in a list. This can be convenient for allowing the user -- to avoid having to specify singleton lists in the configuration file.-oneOrList :: ValueSpecs a -> ValueSpecs [a]+oneOrList :: ValueSpec a -> ValueSpec [a] oneOrList s = pure <$> s <!> listSpec s   -- | The custom specification allows an arbitrary function to be used -- to validate the value extracted by a specification. If 'Nothing' -- is returned the value is considered to have failed validation.-customSpec :: Text -> ValueSpecs a -> (a -> Maybe b) -> ValueSpecs b-customSpec lbl w f = liftValueSpec (CustomSpec lbl (f <$> w))+customSpec :: Text -> ValueSpec a -> (a -> Either Text b) -> ValueSpec b+customSpec lbl w f = primValueSpec (CustomSpec lbl (f <$> w))   -- | Specification for using @yes@ and @no@ to represent booleans 'True' -- and 'False' respectively-yesOrNoSpec :: ValueSpecs Bool+yesOrNoSpec :: ValueSpec Bool yesOrNoSpec = True  <$ atomSpec (Text.pack "yes")           <!> False <$ atomSpec (Text.pack "no") @@ -368,11 +251,106 @@ -- | Matches a non-empty list. -- -- @since 0.2.0.0-nonemptySpec :: ValueSpecs a -> ValueSpecs (NonEmpty a)-nonemptySpec s = customSpec "nonempty" (listSpec s) NonEmpty.nonEmpty+nonemptySpec :: ValueSpec a -> ValueSpec (NonEmpty a)+nonemptySpec s = customSpec "nonempty" (listSpec s) check+  where+    check xs = case NonEmpty.nonEmpty xs of+                 Nothing -> Left "empty list"+                 Just xxs -> Right xxs  -- | Matches a single element or a non-empty list. -- -- @since 0.2.0.0-oneOrNonemptySpec :: ValueSpecs a -> ValueSpecs (NonEmpty a)+oneOrNonemptySpec :: ValueSpec a -> ValueSpec (NonEmpty a) oneOrNonemptySpec s = pure <$> s <!> nonemptySpec s+++------------------------------------------------------------------------+-- 'SectionsSpec' builders+------------------------------------------------------------------------++-- $sections+-- Sections specifications allow you to define an unordered collection+-- of required and optional sections using a convenient 'Applicative'+-- do-notation syntax.+--+-- Let's consider an example of a way to specify a name given a base+-- and optional suffix.+--+-- @+-- {-\# Language OverloadedStrings, ApplicativeDo \#-}+-- module Example where+--+-- import "Config.Schema"+-- import "Data.Text" ('Text')+--+-- nameExample :: 'ValueSpec' 'Text'+-- nameExample =+--   'sectionsSpec' \"name\" '$'+--   do x <- 'reqSection' \"base\" \"Base name\"+--      y <- 'optSection' \"suffix\" \"Optional name suffix\"+--      'pure' ('maybe' x (x '<>') y)+-- @+--+-- Example configuration components and their extracted values.+--+-- > base:     "VAR"+-- > optional: "1"+-- > -- Generates: VAR1+--+-- Order doesn't matter+--+-- > optional: "1"+-- > base:     "VAR"+-- > -- Generates: VAR1+--+-- Optional fields can be omitted+--+-- > base:     "VAR"+-- > -- Generates: VAR+--+-- Unexpected sections will generate errors to help detect typos+--+-- > base:     "VAR"+-- > extra:    0+-- > -- Failure due to unexpected extra section+--+-- All required sections must appear for successful match+--+-- > optional: "1"+-- > -- Failure due to missing required section++-- | Specification for a required section with an implicit value specification.+reqSection ::+  HasSpec a =>+  Text {- ^ section name -} ->+  Text {- ^ description  -} ->+  SectionsSpec a+reqSection n = reqSection' n anySpec+++-- | Specification for a required section with an explicit value specification.+reqSection' ::+  Text        {- ^ section name        -} ->+  ValueSpec a {- ^ value specification -} ->+  Text        {- ^ description         -} ->+  SectionsSpec a+reqSection' n w i = primSectionsSpec (ReqSection n i w)+++-- | Specification for an optional section with an implicit value specification.+optSection ::+  HasSpec a =>+  Text {- ^ section name -} ->+  Text {- ^ description  -} ->+  SectionsSpec (Maybe a)+optSection n = optSection' n anySpec+++-- | Specification for an optional section with an explicit value specification.+optSection' ::+  Text        {- ^ section name        -} ->+  ValueSpec a {- ^ value specification -} ->+  Text        {- ^ description         -} ->+  SectionsSpec (Maybe a)+optSection' n w i = primSectionsSpec (OptSection n i w)
+ src/Config/Schema/Types.hs view
@@ -0,0 +1,185 @@+{-# Language CPP, KindSignatures, RankNTypes, GADTs, DeriveTraversable,+             GeneralizedNewtypeDeriving #-}+{-|+Module      : Config.Schema.Types+Description : Types for describing a configuration file format.+Copyright   : (c) Eric Mertens, 2017+License     : ISC+Maintainer  : emertens@gmail.com++This module defines the syntax of value specifications.++Specifications can be defined using "Config.Schema.Spec" and can be consumed+with "Config.Schema.Load" and "Config.Schema.Doc".++This module defines high-level 'ValueSpec' and 'SectionsSpec' types that are+intended to be used by normal library users. This types are implemented in+terms of primitive 'PrimValueSpec' and 'PrimSectionSpec' types. These+primitives are what consumers of specifications will need to use.++-}+module Config.Schema.Types+  (+  -- * Value specification+    ValueSpec+  , PrimValueSpec(..)+  , primValueSpec+  , runValueSpec+  , runValueSpec_++  -- * Unordered section-value pairs specification+  , SectionsSpec+  , PrimSectionSpec(..)+  , primSectionsSpec+  , runSections+  , runSections_++  ) where++import           Control.Applicative      (Const(..))+import           Control.Applicative.Free (Ap, liftAp, runAp, runAp_)+import           Data.Functor.Alt         (Alt(..))+import           Data.Functor.Coyoneda    (Coyoneda(..), liftCoyoneda, lowerCoyoneda, hoistCoyoneda)+import           Data.List.NonEmpty       (NonEmpty)+import           Data.Semigroup.Foldable  (asum1, foldMap1)+import           Data.Text                (Text)++#if !MIN_VERSION_base(4,11,0)+import           Data.Semigroup           (Semigroup)+#endif++------------------------------------------------------------------------+-- Specifications for values+------------------------------------------------------------------------++-- | The primitive specification descriptions for values. Specifications+-- built from these primitive cases are found in 'ValueSpec'.+data PrimValueSpec :: * -> * where+  -- | Matches any string literal+  TextSpec :: PrimValueSpec Text++  -- | Matches integral numbers+  IntegerSpec :: PrimValueSpec Integer++  -- | Matches any number+  RationalSpec :: PrimValueSpec Rational++  -- | Matches any atom+  AnyAtomSpec :: PrimValueSpec Text++  -- | Specific atom to be matched+  AtomSpec :: Text -> PrimValueSpec ()++  -- | Matches a list of the underlying specification+  ListSpec :: ValueSpec a -> PrimValueSpec [a]++  -- | Documentation identifier and sections specification+  SectionsSpec :: Text -> SectionsSpec a -> PrimValueSpec a++  -- | Matches an arbitrary list of sections. Similar to 'SectionsSpec'+  -- except that that the section names are user-defined.+  AssocSpec :: ValueSpec a -> PrimValueSpec [(Text,a)]++  -- | Documentation text and underlying specification. This specification+  -- will match values where the underlying specification returns a+  -- 'Right' value. Otherwise a 'Left' should contain a short failure +  -- explanation.+  CustomSpec :: Text -> ValueSpec (Either Text a) -> PrimValueSpec a++  -- | Label used to hide complex specifications in documentation.+  NamedSpec :: Text -> ValueSpec a -> PrimValueSpec a++-- | Non-empty disjunction of value specifications. This type is the primary+-- way to specify expected values.+--+-- Multiple specifications can be combined using this type's 'Alt' instance.+newtype ValueSpec a = MkValueSpec+  { unValueSpec :: NonEmpty (Coyoneda PrimValueSpec a) }+  deriving (Functor)++-- | Lift a primitive value specification to 'ValueSpec'.+--+-- @since 0.2.0.0+primValueSpec :: PrimValueSpec a -> ValueSpec a+primValueSpec = MkValueSpec . pure . liftCoyoneda++-- | Given an interpretation of a primitive value specification, extract a list of+-- the possible interpretations of a disjunction of value specifications. Each of+-- these primitive interpretations will be combined using the provided 'Alt' instance.+runValueSpec :: Alt f => (forall x. PrimValueSpec x -> f x) -> ValueSpec a -> f a+runValueSpec f = asum1 . fmap (runCoyoneda f) . unValueSpec+++-- | Given an interpretation of a primitive value specification, extract a list of+-- the possible interpretations of a disjunction of value specifications. Each of+-- these primitive interpretations will be combined using the provided 'Semigroup' instance.+runValueSpec_ :: Semigroup m => (forall x. PrimValueSpec x -> m) -> ValueSpec a -> m+runValueSpec_ f = foldMap1 (runCoyoneda_ f) . unValueSpec+++-- 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)++-- | Left-biased choice between two specifications+instance Alt ValueSpec where MkValueSpec x <!> MkValueSpec y = MkValueSpec (x <!> y)++------------------------------------------------------------------------+-- Specifications for sections+------------------------------------------------------------------------++-- | Specifications for single configuration sections.+--+-- The fields are section name, documentation text, value specification.+-- Use 'ReqSection' for required key-value pairs and 'OptSection' for+-- optional ones.+data PrimSectionSpec :: * -> * where++  -- | Required section: Name, Documentation, Specification+  ReqSection :: Text -> Text -> ValueSpec a -> PrimSectionSpec a++  -- | Optional section: Name, Documentation, Specification+  OptSection :: Text -> Text -> ValueSpec a -> PrimSectionSpec (Maybe a)+++-- | A list of section specifications used to process a whole group of+-- key-value pairs. Multiple section specifications can be combined+-- using this type's 'Applicative' instance.+newtype SectionsSpec a = MkSectionsSpec (Ap PrimSectionSpec a)+  deriving (Functor, Applicative)+++-- | Lift a single specification into a list of specifications.+--+-- @since 0.2.0.0+primSectionsSpec :: PrimSectionSpec a -> SectionsSpec a+primSectionsSpec = MkSectionsSpec . liftAp++-- | Given an function that handles a single, primitive section specification;+-- 'runSections' will generate one that processes a whole 'SectionsSpec'.+--+-- The results from each section will be sequence together using the 'Applicative'+-- instance in of the result type, and the results can be indexed by the type+-- parameter of the specification.+--+-- For an example use of 'runSections', see "Config.Schema.Load".+runSections :: Applicative f => (forall x. PrimSectionSpec x -> f x) -> SectionsSpec a -> f a+runSections f (MkSectionsSpec s) = runAp f s+++-- | Given an function that handles a single, primitive section specification;+-- 'runSections_' will generate one that processes a whole 'SectionsSpec'.+--+-- The results from each section will be sequence together using the 'Monoid'+-- instance in of the result type, and the results will not be indexed by the+-- type parameter of the specifications.+--+-- For an example use of 'runSections_', see "Config.Schema.Docs".+runSections_ :: Monoid m => (forall x. PrimSectionSpec x -> m) -> SectionsSpec a -> m+runSections_ f (MkSectionsSpec s) = runAp_ f s
tests/Main.hs view
@@ -23,9 +23,9 @@ test ::   Show a =>   Eq   a =>-  ValueSpecs a {- ^ specification to match -} ->-  a            {- ^ expected output        -} ->-  [[Text]]     {- ^ inputs sources         -} ->+  ValueSpec a {- ^ specification to match -} ->+  a           {- ^ expected output        -} ->+  [[Text]]    {- ^ inputs sources         -} ->   IO () test spec expected txtss =   for_ txtss $ \txts ->@@ -41,18 +41,18 @@ main :: IO () main = sequenceA_ -  [ test valuesSpec ("Hello world"::Text)+  [ test anySpec ("Hello world"::Text)     [["\"Hello world\""]     ,["\"H\\101l\\&l\\o157 \\"      ,"  \\w\\x6frld\""]     ] -  , test valuesSpec (1234::Integer)+  , test anySpec (1234::Integer)     [["1234"]     ,["1234.0"]     ] -  , test valuesSpec (0.65::Rational)+  , test anySpec (0.65::Rational)     [["0.65e0"]     ,["65e-2"]     ,["6.5e-1"]@@ -65,7 +65,7 @@   , test (atomSpec "testing-1-2-3") ()     [["testing-1-2-3"]] -  , test (listSpec valuesSpec) ([]::[Integer])+  , test (listSpec anySpec) ([]::[Integer])     [["[]"]     ,["[ ]"]] @@ -75,7 +75,7 @@     ,["* ḿyatoḿ"]     ] -  , test valuesSpec [1,2,3::Int]+  , test anySpec [1,2,3::Int]     [["[1,2,3]"]     ,["[1,2,3,]"]     ,["* 1"@@ -83,7 +83,7 @@      ,"* 3"]     ] -  , test (listSpec valuesSpec) [[1,2],[3,4::Int]]+  , test (listSpec anySpec) [[1,2],[3,4::Int]]     [["[[1,2,],[3,4]]"]     ,["*[1,2]"      ,"*[3,4]"]@@ -94,19 +94,19 @@      ]     ] -  , test (assocSpec valuesSpec) ([]::[(Text,Int)])+  , test (assocSpec anySpec) ([]::[(Text,Int)])     [["{}"]     ,["{ }"]     ] -  , test (assocSpec valuesSpec) [("k1",10::Int), ("k2",20)]+  , test (assocSpec anySpec) [("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) ]+  , test anySpec [ Left (1::Int), Right ("two"::Text) ]     [["[1, \"two\"]"]     ,["* 1"      ,"* \"two\""]