packages feed

gendocs (empty) → 0.1.0.0

raw patch · 11 files changed

+483/−0 lines, 11 filesdep +aesondep +aeson-prettydep +basesetup-changed

Dependencies added: aeson, aeson-pretty, base, bytestring, gendocs, safe, text

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Sean Hess (c) 2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Sean Hess nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,104 @@+Generate interface documentation for your types++Example+-------++    {-# LANGUAGE OverloadedStrings #-}+    {-# LANGUAGE DeriveGeneric #-}+    {-# LANGUAGE GeneralizedNewtypeDeriving #-}+    module Example where++    import Data.Aeson (ToJSON)+    import Data.Proxy (Proxy(..))+    import Data.Docs (markdown, Docs(..), Sample(..), genDocs)+    import Data.Text (Text)+    import qualified Data.Text as Text+    import GHC.Generics (Generic)++    data Person = Person+        { name :: Text+        , age :: Age+        , hobbies :: [Hobby]+        } deriving (Show, Eq, Generic)+    instance ToJSON Person+    instance Docs Person+    instance Sample Person where+        sample _ = Person+          { name = "Bob"+          , age = sample (Proxy :: Proxy Age)+          , hobbies = [Haskell, Friends]+          }++    newtype Age = Age Int+        deriving (Show, Eq, Generic)+    instance ToJSON Age+    instance Docs Age where+        docs = genDocs "Age in years"+    instance Sample Age where+        sample _ = Age 31+        samples _ = [Age 31, Age 24]++    data Hobby = Haskell | Friends | Movies+        deriving (Show, Eq, Generic, Enum, Bounded)+    instance ToJSON Hobby+    instance Docs Hobby+    instance Sample Hobby where+        sample _ = Haskell+        allValues _ = [minBound..]++    doc :: IO ()+    doc = do+        putStrLn $ Text.unpack json++    json :: Text+    json = Text.intercalate "\n\n\n" $+      [ markdown (Proxy :: Proxy Person)+      , markdown (Proxy :: Proxy Age)+      , markdown (Proxy :: Proxy Hobby)+      ]++This generates the following output++    Person+    ---------------------++    | Field   | Type              |+    |---------|-------------------|+    | name    | [Text](#text)     |+    | age     | [Age](#age)       |+    | hobbies | [[Hobby]](#hobby) |++    ```+    {+        "age": 31,+        "hobbies": [+            "Haskell",+            "Friends"+        ],+        "name": "Bob"+    }+    ```+++    Age+    ---------------------++    Age in years++    ```+    31+    ```+++    Hobby+    ---------------------++    | Values    |+    |-----------|+    | "Haskell" |+    | "Friends" |+    | "Movies"  |++    ```+    "Haskell"+    ```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ gendocs.cabal view
@@ -0,0 +1,45 @@+name:                gendocs+version:             0.1.0.0+synopsis:            Library for generating interface documentation from types+description:         Please see README.md+homepage:            https://github.com/seanhess/gendocs#readme+license:             BSD3+license-file:        LICENSE+author:              Sean Hess+maintainer:          seanhess@gmail.com+copyright:           Orbital Labs+category:            Web+build-type:          Simple+extra-source-files:  README.md+cabal-version:       >=1.10++library+  hs-source-dirs:      src+  exposed-modules:+      Data.Docs+    , Data.Docs.Docs+    , Data.Docs.ToMarkdown+    , Data.Docs.Sample+    , Data.Docs.Selectors+    , Data.Docs.TypeName+  build-depends:+      base >= 4.7 && < 5+    , aeson+    , aeson-pretty+    , bytestring+    , safe+    , text+  default-language:    Haskell2010++test-suite gendocs-test+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Spec.hs+  build-depends:       base+                     , gendocs+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N+  default-language:    Haskell2010++source-repository head+  type:     git+  location: https://github.com/seanhess/gendocs
+ src/Data/Docs.hs view
@@ -0,0 +1,14 @@+module Data.Docs+  ( Documentation(..)+  , Field(..)+  , Docs(..)+  , genDocs+  , genValues+  , genFields+  , Sample(..)+  , markdown+  ) where++import Data.Docs.Docs+import Data.Docs.Sample+import Data.Docs.ToMarkdown (markdown)
+ src/Data/Docs/Docs.hs view
@@ -0,0 +1,73 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}++module Data.Docs.Docs+  ( Documentation(..)+  , Field(..)+  , Docs(..)+  , genDocs+  , genValues+  , genFields+  ) where++import Data.Text (Text, pack)+import qualified Data.Text as T+import Data.Docs.TypeName (GTypeName)+import qualified Data.Docs.TypeName as TypeName+import Data.Docs.Selectors (Selectors(..))+import Data.Data (Proxy(..), TypeRep)+import GHC.Generics (Generic, Rep)+++-- | Can generate documentation for type+class Docs a where+    docs :: Proxy a -> Documentation+    default docs :: (Generic a, GTypeName (Rep a), Selectors (Rep a)) => Proxy a -> Documentation+    docs = genDocs ""+++type FieldName = Text+type TypeName = Text++-- | Documentation for a given type+data Documentation = Documentation+    { typeName :: TypeName+    , fields :: [Field]+    , description :: Text+    } deriving (Show, Eq)++-- | Documentation for a record field+data Field = Field+    { fieldName :: FieldName+    , fieldType :: TypeName+    , isRequired :: Bool+    } deriving (Show, Eq)+++-- | Create documentation for a type with only a description+genDocs :: forall a. (Generic a, GTypeName (Rep a), Selectors (Rep a)) => Text -> Proxy a -> Documentation+genDocs d p = Documentation+    { typeName = pack $ TypeName.typeName p+    , description = d+    , fields = genFields p+    }+++genFields :: forall a. (Selectors (Rep a)) => Proxy a -> [Field]+genFields _ = map (uncurry toField . selectorToText) $ selectors (Proxy :: Proxy (Rep a))+++genValues :: forall a. (Enum a, Bounded a, Show a) => Proxy a -> [Text]+genValues _ = map (pack . show) ([minBound..] :: [a])+++selectorToText :: (String, TypeRep) -> (Text, [Text])+selectorToText (s, t) = (pack s, T.words $ pack $ show t)+++toField :: Text -> [Text] -> Field+toField s ("Maybe":t) = Field s (T.concat t) False+toField s t = Field s (T.concat t) True
+ src/Data/Docs/Sample.hs view
@@ -0,0 +1,15 @@+-- a list type is annoying++module Data.Docs.Sample where++import Data.Proxy (Proxy)++class Sample a where+    sample :: Proxy a -> a++    samples :: Proxy a -> [a]+    samples p = [sample p]++    -- | If the values are bounded (an enum), list all of them+    allValues :: Proxy a -> [a]+    allValues _ = []
+ src/Data/Docs/Selectors.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Data.Docs.Selectors where++import Data.Data (Proxy(..), TypeRep, Typeable, typeOf)+import GHC.Generics++-- data Record = Record { recordId :: Int32, recordName :: Text }+--   deriving (Generic)+-- selectors (Proxy :: Proxy (Rep Record))++class Selectors rep where+  selectors :: Proxy rep -> [(String, TypeRep)]++instance Selectors f => Selectors (M1 D x f) where+  selectors _ = selectors (Proxy :: Proxy f)++instance Selectors f => Selectors (M1 C x f) where+  selectors _ = selectors (Proxy :: Proxy f)++-- instance Selectors f => Selectors (C1 C x f) where+--   selectors _ = selectors (Proxy :: Proxy f)++instance (Selector s, Typeable t) => Selectors (M1 S s (K1 R t)) where+  selectors _ =+    let sel = selName (undefined :: M1 S s (K1 R t) ())+        typ = typeOf (undefined :: t)+    in if not (null sel)+      then [( sel , typ )]+      else []++instance (Selectors a, Selectors b) => Selectors (a :*: b) where+  selectors _ = selectors (Proxy :: Proxy a) ++ selectors (Proxy :: Proxy b)++-- | We don't really want to deal with sum types+instance (Selectors a, Selectors b) => Selectors (a :+: b) where+  selectors _ = selectors (Proxy :: Proxy a) ++ selectors (Proxy :: Proxy b)++instance Selectors U1 where+  selectors _ = []
+ src/Data/Docs/ToMarkdown.hs view
@@ -0,0 +1,132 @@+{-# LANGUAGE OverloadedStrings #-}+module Data.Docs.ToMarkdown where++import Data.Aeson (ToJSON, toJSON, Value, encode)+import Data.Aeson.Encode.Pretty (encodePretty', defConfig, Config(..))+import Data.Char (isAlphaNum)+import Data.Data (Proxy(..))+import Data.Docs.Docs (Documentation(..), Field(..), Docs(..))+import Data.Docs.Sample (Sample(..))+import qualified Data.List as List+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text+import qualified Data.ByteString.Lazy as BSL+import Data.Monoid ((<>))+import Data.Maybe (fromMaybe)+import Safe (maximumDef, headMay)++-- | Generate markdown documentation+markdown :: (ToJSON a, Docs a, Sample a) => Proxy a -> Text+markdown p =+  let d = docs p+      s = sample p+      vs =  allValues p+  in Text.intercalate "\n\n" $ map (Text.intercalate "\n") $ filter (not . List.null)+  [ header d+  , desc (description d)+  , fieldTable (fields d)+  , valuesTable (map toJSON vs)+  , example s+  ]++desc :: Text -> [Text]+desc "" = []+desc d = [d]++header :: Documentation -> [Text]+header d =+  [ typeName d+  , "---------------------"+  ]++example :: ToJSON a => a -> [Text]+example a =+  [ "```"+  , Text.decodeUtf8 $ BSL.toStrict $ encodePretty' conf a+  , "```"+  ]+  where+    conf = defConfig { confCompare = compare }+++type URL = Text++link :: Text -> URL -> Text+link t u =+    "[" <> t <> "](" <> u <> ")"++anchor :: Text -> Text -> Text+anchor t n = link t $ "#" <> (Text.filter isAlphaNum $ Text.toLower n)+++fieldTable :: [Field] -> [Text]+fieldTable [] = []+fieldTable fs =+  table ["Field", "Type", ""] (map row fs)+  where+    row f =+      [ fieldName f+      , anchor (fieldType f) (fieldType f)+      , opInfo f+      ]+    opInfo f =+      if isRequired f+        then ""+        else "(optional)"+++valuesTable :: [Value] -> [Text]+valuesTable [] = []+valuesTable vs =+  table ["Values"] (map row vs)+  where+    row t = [ Text.decodeUtf8 $ BSL.toStrict $ encode t ]+++-- | Tables++type CellWidth = Int+++cellWidth :: [Text] -> Int+cellWidth ts = maximumDef 0 (map Text.length ts)+++table :: [Text] -> [[Text]] -> [Text]+table hs rs =+  let jr = List.transpose $ map justifyColumn $ filter nonEmptyCol $ List.transpose (hs : rs) :: [[Text]]+      hs' = fromMaybe [] $ headMay jr :: [Text]+      rs' = drop 1 jr+  in mconcat [ tableHeader hs', map tableRow rs' ]+  where+    nonEmptyCol = not . all Text.null . List.drop 1++      -- calculate the column width for a column+++justifyColumn :: [Text] -> [Text]+justifyColumn ts =+  let w = maximumDef 0 (map Text.length ts)+  in map (Text.justifyLeft w ' ') ts++++tableHeader :: [Text] -> [Text]+tableHeader ts =+  [tableRow ts, sepRow ts]+++tableRow :: [Text] -> Text+tableRow ts =+  mconcat ["| ", Text.intercalate " | " ts, " |"]+  where+++sepRow :: [Text] -> Text+sepRow ts =+  mconcat ["|-", Text.intercalate "-|-" (map sepCell ts), "-|"]+  where+    sepCell t = Text.replicate (Text.length t) "-"++
+ src/Data/Docs/TypeName.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Data.Docs.TypeName+  ( typeName+  , GTypeName(gtypename)+  ) where++import Data.Data (Proxy(..))+import GHC.Generics+++-- | Get the typename for anything that has a simple one. https://gist.github.com/nh2/1a03b7873dbed348ef64fe536028776d++typeName :: forall a. (Generic a, GTypeName (Rep a)) => Proxy a -> String+typeName _proxy = gtypename (from (undefined :: a))++class GTypeName f where+  gtypename :: f a -> String++instance (Datatype c) => GTypeName (M1 i c f) where+  gtypename m = datatypeName m
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"