{"action":"created","comment":{"url":"https://api.github.com/repos/liqd/thentos/pulls/comments/27259683","id":27259683,"diff_hunk":"@@ -1,312 +1,199 @@\n-{-# LANGUAGE OverloadedStrings                        #-}\n-{-# LANGUAGE ScopedTypeVariables                      #-}\n-{-# LANGUAGE TupleSections                            #-}\n+{-# LANGUAGE DataKinds            #-}\n+{-# LANGUAGE DeriveDataTypeable   #-}\n+{-# LANGUAGE DeriveGeneric        #-}\n+{-# LANGUAGE OverloadedStrings    #-}\n+{-# LANGUAGE ScopedTypeVariables  #-}\n+{-# LANGUAGE TupleSections        #-}\n+{-# LANGUAGE TypeOperators        #-}\n \n--- | FIXME: we need to find a less verbose way to combine command line\n--- arguments with data from config file.  See also: #1673, #1694.\n module Thentos.Config\n-    ( getCommand\n-    , configLogger\n-    , Command(..)\n-    , ThentosConfig(..)\n-    , FrontendConfig(..)\n-    , BackendConfig(..)\n-    , ProxyConfig(..)\n-    , SmtpConfig(..)\n-    , emptySmtpConfig\n-    , emptyThentosConfig\n-    ) where\n-\n-import Control.Applicative (pure, (<$>), (<*>), (<|>), optional)\n-import Control.Monad (join)\n-import Data.Map (Map)\n+where\n+\n+import Control.Applicative ((<$>), (<|>))\n+import Control.Exception (throwIO)\n+import Data.Configifier ((:>), (:*>)((:*>)), (:>:), (>>.))\n+import Data.Configifier (configify', renderConfigFile, docs)\n+import Data.Configifier (NoDesc, ToConfigCode, ToConfig, Source(..), Tagged(Tagged), TaggedM(TaggedM), Result, MaybeO(..))\n import Data.Maybe (fromMaybe)\n-import Data.Monoid (Monoid(..), (<>))\n+import Data.Proxy (Proxy(Proxy))\n+import Data.String.Conversions (ST, cs, (<>))\n+import Data.Typeable (Typeable)\n+import GHC.Generics (Generic)\n import Network.Mail.Mime (Address(Address))\n-import Options.Applicative (command, info, progDesc, long, short, auto, option, flag, help)\n-import Options.Applicative (Parser, execParser, metavar, subparser)\n-import Safe (readDef)\n import System.Directory (createDirectoryIfMissing)\n+import System.Environment (getEnvironment, getArgs)\n import System.FilePath (takeDirectory)\n import System.IO (stderr)\n import System.Log.Formatter (simpleLogFormatter)\n import System.Log.Handler.Simple (formatter, fileHandler, streamHandler)\n-import System.Log.Logger (Priority(DEBUG), removeAllHandlers, updateGlobalLogger, setLevel, setHandlers)\n-import System.Log.Missing (loggerName)\n+import System.Log.Logger (Priority(DEBUG, CRITICAL), removeAllHandlers, updateGlobalLogger, setLevel, setHandlers)\n+import System.Log.Missing (loggerName, logger)\n+import Text.Show.Pretty (ppShow)\n \n-import qualified Data.Configurator as Configurator\n-import qualified Data.Configurator.Types as Configurator\n-import qualified Data.HashMap.Strict as HM\n+import qualified Data.Aeson as Aeson\n+import qualified Data.ByteString as SBS\n import qualified Data.Map as Map\n+import qualified Data.Text.IO as ST\n+import qualified Generics.Generic.Aeson as Aeson\n \n import Thentos.Types\n \n \n--- * the config type used by everyone else\n-\n-data ThentosConfig = ThentosConfig\n-    { frontendConfig :: Maybe FrontendConfig\n-    , backendConfig :: Maybe BackendConfig\n-    , proxyConfig :: Maybe ProxyConfig\n-    , smtpConfig :: SmtpConfig\n-    , defaultUser :: Maybe (UserFormData, [Role])\n-    }\n-  deriving (Eq)\n-\n-data BackendConfig = BackendConfig { backendPort :: Int }\n-  deriving (Eq, Show)\n-\n-data FrontendConfig = FrontendConfig { frontendPort :: Int }\n-  deriving (Eq, Show)\n-\n-data ProxyConfig = ProxyConfig { proxyTargets :: Map ServiceId String }\n-  deriving (Eq, Show)\n-\n-data SmtpConfig = SmtpConfig\n-    { smtpSender       :: Address\n-    , smtpSendmailPath :: FilePath\n-    , smtpSendmailArgs :: [String]\n-    }\n-  deriving (Eq, Show)\n-\n-emptySmtpConfig :: SmtpConfig\n-emptySmtpConfig = case mempty of\n-    SmtpConfigBuilder mSender mProg mArgs -> SmtpConfig\n-        (fromMaybe (Address Nothing \"\") mSender)\n-        (fromMaybe \"/usr/sbin/sendmail\" mProg)\n-        (fromMaybe [\"-t\"] mArgs)\n-\n-emptyThentosConfig :: ThentosConfig\n-emptyThentosConfig = ThentosConfig Nothing Nothing Nothing emptySmtpConfig Nothing\n-\n-\n--- * combining (partial) configurations from multiple sources\n-\n-data ThentosConfigBuilder = ThentosConfigBuilder\n-    { bRunFrontend :: Maybe Bool\n-    , bRunBackend :: Maybe Bool\n-    , bBackendConfig :: BackendConfigBuilder\n-    , bFrontendConfig :: FrontendConfigBuilder\n-    , bProxyConfig :: ProxyConfigBuilder\n-    , bSmtpConfig :: SmtpConfigBuilder\n-    , bDefaultUser :: Maybe (UserFormData, [Role])\n-    }\n-\n-data BackendConfigBuilder = BackendConfigBuilder { bBackendPort :: Maybe Int }\n-data FrontendConfigBuilder = FrontendConfigBuilder { bFrontendPort :: Maybe Int }\n-data ProxyConfigBuilder = ProxyConfigBuilder\n-    { bProxyTarget :: Maybe (Map ServiceId String) }\n-data SmtpConfigBuilder = SmtpConfigBuilder (Maybe Address) (Maybe FilePath) (Maybe [String])\n-\n-instance Monoid BackendConfigBuilder where\n-    mempty = BackendConfigBuilder Nothing\n-    b1 `mappend` b2 =\n-        BackendConfigBuilder\n-            { bBackendPort = bBackendPort b1 <|> bBackendPort b2 }\n-\n-instance Monoid FrontendConfigBuilder where\n-    mempty = FrontendConfigBuilder Nothing\n-    b1 `mappend` b2 =\n-        FrontendConfigBuilder\n-            { bFrontendPort = bFrontendPort b1 <|> bFrontendPort b2 }\n-\n-instance Monoid ProxyConfigBuilder where\n-    mempty = ProxyConfigBuilder Nothing\n-    b1 `mappend` b2 =\n-        ProxyConfigBuilder\n-            { bProxyTarget = bProxyTarget b1 <|> bProxyTarget b2 }\n-\n-instance Monoid SmtpConfigBuilder where\n-    mempty = SmtpConfigBuilder Nothing Nothing Nothing\n-    (SmtpConfigBuilder sender prog args) `mappend` (SmtpConfigBuilder sender' prog' args') =\n-        SmtpConfigBuilder (sender <|> sender') (prog <|> prog') (args <|> args')\n-\n-instance Monoid ThentosConfigBuilder where\n-    mempty = ThentosConfigBuilder Nothing Nothing mempty mempty mempty mempty Nothing\n-    b1 `mappend` b2 =\n-        ThentosConfigBuilder\n-            (bRunFrontend b1 <|> bRunFrontend b2)\n-            (bRunBackend b1 <|> bRunBackend b2)\n-            (bBackendConfig b1 <> bBackendConfig b2)\n-            (bFrontendConfig b1 <> bFrontendConfig b2)\n-            (bProxyConfig b1 <> bProxyConfig b2)\n-            (bSmtpConfig b1 <> bSmtpConfig b2)\n-            (bDefaultUser b1 <|> bDefaultUser b2)\n-\n-data ConfigError =\n-    FrontendConfigMissing\n-  | BackendConfigMissing\n-  | UnknownRoleForDefaultUser String\n-  | NoEmailSender\n-  deriving (Eq, Show)\n-\n-finaliseConfig :: ThentosConfigBuilder -> Either ConfigError ThentosConfig\n-finaliseConfig builder =\n-    ThentosConfig\n-        <$> frontendConf\n-        <*> backendConf\n-        <*> proxyConf\n-        <*> smtpConf\n-        <*> defaultUserConf\n-  where\n-    smtpConf = case bSmtpConfig builder of\n-        SmtpConfigBuilder (Just sender) mProg mArgs ->\n-            case emptySmtpConfig of\n-                SmtpConfig _ defProg defArgs -> Right $\n-                    SmtpConfig sender (fromMaybe defProg mProg) (fromMaybe defArgs mArgs)\n-        SmtpConfigBuilder Nothing _ _ -> Left NoEmailSender\n-    backendConf = case (bRunBackend builder, finaliseBackendConfig $ bBackendConfig builder) of\n-        (Just True, Nothing) -> Left BackendConfigMissing\n-        (Just True, bConf) -> Right bConf\n-        _ -> Right Nothing\n-    frontendConf = case (bRunFrontend builder, finaliseFrontendConfig $ bFrontendConfig builder) of\n-        (Just True, Nothing) -> Left FrontendConfigMissing\n-        (Just True, fConf) -> Right fConf\n-        _ -> Right Nothing\n-    proxyConf = Right . finaliseProxyConfig $ bProxyConfig builder\n-    defaultUserConf = return $ bDefaultUser builder\n-\n-finaliseFrontendConfig :: FrontendConfigBuilder -> Maybe FrontendConfig\n-finaliseFrontendConfig builder = FrontendConfig <$> bFrontendPort builder\n-\n-finaliseBackendConfig :: BackendConfigBuilder -> Maybe BackendConfig\n-finaliseBackendConfig builder = BackendConfig <$> bBackendPort builder\n-\n-finaliseProxyConfig :: ProxyConfigBuilder -> Maybe ProxyConfig\n-finaliseProxyConfig builder = ProxyConfig <$> bProxyTarget builder\n-\n-finaliseCommand :: FilePath -> CommandBuilder -> IO (Either ConfigError Command)\n-finaliseCommand _ BShowDB = return $ Right ShowDB\n-finaliseCommand filePath (BRun cmdLineConfigBuilder) = do\n-    fileConfigBuilder <- parseConfigFile filePath\n-    let finalConfig = finaliseConfig $ cmdLineConfigBuilder <> fileConfigBuilder\n-    return $ Run <$> finalConfig\n-finaliseCommand filePath (BRunA3 cmdLineConfigBuilder) = do\n-    fileConfigBuilder <- parseConfigFile filePath\n-    let finalConfig = finaliseConfig $ cmdLineConfigBuilder <> fileConfigBuilder\n-    return $ RunA3 <$> finalConfig\n-\n-getCommand :: FilePath -> IO (Either ConfigError Command)\n-getCommand filePath = do\n-    cmdLineBuilder <- parseCommandBuilder\n-    finaliseCommand filePath cmdLineBuilder\n-\n-\n--- * command line parsing\n-\n-parseCommandBuilder :: IO CommandBuilder\n-parseCommandBuilder = execParser opts\n-  where\n-    parser = subparser $ command \"run\" (info parseRun (progDesc \"run\")) <>\n-                         command \"runa3\" (info parseRunA3 (progDesc \"run with a3 backend\")) <>\n-                         command \"showdb\" (info (pure BShowDB) (progDesc \"show\"))\n-    opts = info parser mempty\n-\n-data Command = Run ThentosConfig | RunA3 ThentosConfig | ShowDB\n-  deriving (Eq)\n-\n-data CommandBuilder =\n-    BRun ThentosConfigBuilder | BRunA3 ThentosConfigBuilder | BShowDB\n-\n-parseRun :: Parser CommandBuilder\n-parseRun = BRun <$> parseThentosConfig\n-\n-parseRunA3 :: Parser CommandBuilder\n-parseRunA3 = BRunA3 <$> parseThentosConfig\n-\n-parseThentosConfig :: Parser ThentosConfigBuilder\n-parseThentosConfig =\n-    ThentosConfigBuilder <$>\n-        parseRunFrontend <*>\n-        parseRunBackend <*>\n-        parseBackendConfigBuilder <*>\n-        parseFrontendConfigBuilder <*>\n-        parseProxyConfigBuilder <*>\n-        pure mempty <*>\n-        pure Nothing\n-\n-parseBackendConfigBuilder :: Parser BackendConfigBuilder\n-parseBackendConfigBuilder =\n-    BackendConfigBuilder <$> optional parseBackendPort\n+-- * config structure\n+\n+type ThentosConfig         = Tagged ThentosConfigUntagged\n+type ThentosConfigUntagged = NoDesc ThentosConfigDesc\n+type ThentosConfigDesc     = ToConfigCode ThentosConfig'\n+\n+type ThentosConfig' =\n+            (\"command\"      :> Command)           :>: \"One of 'run', runA3, 'showDB'.\"\n+  :*> Maybe (\"frontend\"     :> HttpConfig'        :>: \"HTTP server for html forms.\")\n+  :*> Maybe (\"backend\"      :> HttpConfig'        :>: \"HTTP server for rest api.\")\n+  :*> Maybe (\"proxies\"      :> [HttpProxyConfig'] :>: \"HTTP server for tunneling requests to services.\")\n+        -- FIXME: make proxies a map keyed by service ids\n+  :*>       (\"smtp\"         :> SmtpConfig')       :>: \"Sending email.\"\n+  :*> Maybe (\"default_user\" :> DefaultUserConfig' :>: \"A user that is created if the user table is empty.\")\n+\n+defaultThentosConfig :: ToConfig ThentosConfigUntagged Maybe\n+defaultThentosConfig =\n+      Just Run\n+  :*> NothingO\n+  :*> NothingO\n+  :*> NothingO\n+  :*> Just defaultSmtpConfig\n+  :*> NothingO\n+\n+type HttpConfig = Tagged (NoDesc (ToConfigCode HttpConfig'))\n+type HttpConfig' =\n+      Maybe (\"bind_schema\"   :> HttpSchema)\n+  :*>       (\"bind_host\"     :> ST)\n+  :*>       (\"bind_port\"     :> Int)\n+  :*> Maybe (\"expose_schema\" :> HttpSchema)\n+  :*> Maybe (\"expose_host\"   :> ST)\n+  :*> Maybe (\"expose_port\"   :> Int)\n+\n+type HttpProxyConfig = Tagged (NoDesc (ToConfigCode HttpProxyConfig'))\n+type HttpProxyConfig' =\n+            (\"service_id\" :> ST)\n+  :*>       (\"http\"       :> HttpConfig')\n+  :*> Maybe (\"url_prefix\" :> ST)\n+\n+type SmtpConfig = Tagged (NoDesc (ToConfigCode SmtpConfig'))\n+type SmtpConfig' =\n+      Maybe (\"sender_name\"    :> ST)  -- FIXME: use more specific type 'Network.Mail.Mime.Address'\n+  :*>       (\"sender_address\" :> ST)\n+  :*>       (\"sendmail_path\"  :> ST)\n+  :*>       (\"sendmail_args\"  :> [ST])\n+\n+defaultSmtpConfig :: ToConfig (NoDesc (ToConfigCode SmtpConfig')) Maybe\n+defaultSmtpConfig =\n+      NothingO\n+  :*> Nothing\n+  :*> Just \"/usr/sbin/sendmail\"\n+  :*> Just [\"-t\"]\n+\n+type DefaultUserConfig = Tagged (NoDesc (ToConfigCode DefaultUserConfig'))\n+type DefaultUserConfig' =\n+            (\"name\"     :> ST)  -- FIXME: use more specific type?\n+  :*>       (\"password\" :> ST)  -- FIXME: use more specific type?\n+  :*>       (\"email\"    :> ST)  -- FIXME: use more specific type 'Network.Mail.Mime.Address'\n+  :*> Maybe (\"roles\"    :> [Role])\n+\n+\n+-- * leaf types\n+\n+data Command = Run | RunA3 | ShowDB\n+  deriving (Eq, Ord, Show, Enum, Bounded, Typeable, Generic)\n+\n+instance Aeson.ToJSON Command where toJSON = Aeson.gtoJson\n+instance Aeson.FromJSON Command where parseJSON = Aeson.gparseJson\n+\n+data HttpSchema = Http | Https\n+  deriving (Eq, Ord, Enum, Bounded, Typeable, Generic)\n+\n+instance Show HttpSchema where\n+    show Http = \"http\"\n+    show Https = \"https\"\n+\n+instance Aeson.ToJSON HttpSchema where toJSON = Aeson.gtoJson\n+instance Aeson.FromJSON HttpSchema where parseJSON = Aeson.gparseJson\n+\n+\n+-- * driver\n+\n+printConfigUsage :: IO ()\n+printConfigUsage = do\n+    -- ST.putStrLn $ docs (Proxy :: Proxy ThentosConfigDesc)\n+    ST.putStrLn $ docs (Proxy :: Proxy (ToConfigCode ThentosConfig'))\n+\n+\n+getConfig :: FilePath -> IO ThentosConfig\n+getConfig configFile = do\n+    sources <- sequence\n+        [ ConfigFileYaml <$> SBS.readFile configFile\n+        , ShellEnv       <$> getEnvironment\n+        , CommandLine    <$> getArgs\n+        ]\n+    logger DEBUG $ \"config sources:\\n\" ++ ppShow sources\n+\n+    case configify' (TaggedM defaultThentosConfig) sources :: Result ThentosConfigUntagged of\n+        Left e -> do\n+            logger CRITICAL $ \"error parsing config: \" ++ show e","path":"src/Thentos/Config.hs","position":356,"original_position":356,"commit_id":"9023c4730b12ff8c4979ed33cb626de97dbb80d5","original_commit_id":"af80cddb2a97c359a79d09e20a929d4b875fbaff","user":{"login":"mf59816","id":216437,"avatar_url":"https://avatars.githubusercontent.com/u/216437?v=3","gravatar_id":"","url":"https://api.github.com/users/mf59816","html_url":"https://github.com/mf59816","followers_url":"https://api.github.com/users/mf59816/followers","following_url":"https://api.github.com/users/mf59816/following{/other_user}","gists_url":"https://api.github.com/users/mf59816/gists{/gist_id}","starred_url":"https://api.github.com/users/mf59816/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mf59816/subscriptions","organizations_url":"https://api.github.com/users/mf59816/orgs","repos_url":"https://api.github.com/users/mf59816/repos","events_url":"https://api.github.com/users/mf59816/events{/privacy}","received_events_url":"https://api.github.com/users/mf59816/received_events","type":"User","site_admin":false},"body":"having the error message would help.  (-:","created_at":"2015-03-26T21:48:50Z","updated_at":"2015-03-26T21:48:50Z","html_url":"https://github.com/liqd/thentos/pull/18#discussion_r27259683","pull_request_url":"https://api.github.com/repos/liqd/thentos/pulls/18","_links":{"self":{"href":"https://api.github.com/repos/liqd/thentos/pulls/comments/27259683"},"html":{"href":"https://github.com/liqd/thentos/pull/18#discussion_r27259683"},"pull_request":{"href":"https://api.github.com/repos/liqd/thentos/pulls/18"}}},"pull_request":{"url":"https://api.github.com/repos/liqd/thentos/pulls/18","id":31821023,"html_url":"https://github.com/liqd/thentos/pull/18","diff_url":"https://github.com/liqd/thentos/pull/18.diff","patch_url":"https://github.com/liqd/thentos/pull/18.patch","issue_url":"https://api.github.com/repos/liqd/thentos/issues/18","number":18,"state":"open","locked":false,"title":"Move from configurator, optparse-applicative to configifier.","user":{"login":"mf59816","id":216437,"avatar_url":"https://avatars.githubusercontent.com/u/216437?v=3","gravatar_id":"","url":"https://api.github.com/users/mf59816","html_url":"https://github.com/mf59816","followers_url":"https://api.github.com/users/mf59816/followers","following_url":"https://api.github.com/users/mf59816/following{/other_user}","gists_url":"https://api.github.com/users/mf59816/gists{/gist_id}","starred_url":"https://api.github.com/users/mf59816/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mf59816/subscriptions","organizations_url":"https://api.github.com/users/mf59816/orgs","repos_url":"https://api.github.com/users/mf59816/repos","events_url":"https://api.github.com/users/mf59816/events{/privacy}","received_events_url":"https://api.github.com/users/mf59816/received_events","type":"User","site_admin":false},"body":"There is still room for improvement, but this is my fist shot.\r\n","created_at":"2015-03-24T12:24:02Z","updated_at":"2015-03-26T21:48:50Z","closed_at":null,"merged_at":null,"merge_commit_sha":"8c316f64eeb451d715803f17691252fe7c146a2d","assignee":{"login":"mf59816","id":216437,"avatar_url":"https://avatars.githubusercontent.com/u/216437?v=3","gravatar_id":"","url":"https://api.github.com/users/mf59816","html_url":"https://github.com/mf59816","followers_url":"https://api.github.com/users/mf59816/followers","following_url":"https://api.github.com/users/mf59816/following{/other_user}","gists_url":"https://api.github.com/users/mf59816/gists{/gist_id}","starred_url":"https://api.github.com/users/mf59816/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mf59816/subscriptions","organizations_url":"https://api.github.com/users/mf59816/orgs","repos_url":"https://api.github.com/users/mf59816/repos","events_url":"https://api.github.com/users/mf59816/events{/privacy}","received_events_url":"https://api.github.com/users/mf59816/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/liqd/thentos/pulls/18/commits","review_comments_url":"https://api.github.com/repos/liqd/thentos/pulls/18/comments","review_comment_url":"https://api.github.com/repos/liqd/thentos/pulls/comments{/number}","comments_url":"https://api.github.com/repos/liqd/thentos/issues/18/comments","statuses_url":"https://api.github.com/repos/liqd/thentos/statuses/9023c4730b12ff8c4979ed33cb626de97dbb80d5","head":{"label":"liqd:2015-03-23-mf-configifier+1","ref":"2015-03-23-mf-configifier+1","sha":"9023c4730b12ff8c4979ed33cb626de97dbb80d5","user":{"login":"liqd","id":2312088,"avatar_url":"https://avatars.githubusercontent.com/u/2312088?v=3","gravatar_id":"","url":"https://api.github.com/users/liqd","html_url":"https://github.com/liqd","followers_url":"https://api.github.com/users/liqd/followers","following_url":"https://api.github.com/users/liqd/following{/other_user}","gists_url":"https://api.github.com/users/liqd/gists{/gist_id}","starred_url":"https://api.github.com/users/liqd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/liqd/subscriptions","organizations_url":"https://api.github.com/users/liqd/orgs","repos_url":"https://api.github.com/users/liqd/repos","events_url":"https://api.github.com/users/liqd/events{/privacy}","received_events_url":"https://api.github.com/users/liqd/received_events","type":"Organization","site_admin":false},"repo":{"id":31423016,"name":"thentos","full_name":"liqd/thentos","owner":{"login":"liqd","id":2312088,"avatar_url":"https://avatars.githubusercontent.com/u/2312088?v=3","gravatar_id":"","url":"https://api.github.com/users/liqd","html_url":"https://github.com/liqd","followers_url":"https://api.github.com/users/liqd/followers","following_url":"https://api.github.com/users/liqd/following{/other_user}","gists_url":"https://api.github.com/users/liqd/gists{/gist_id}","starred_url":"https://api.github.com/users/liqd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/liqd/subscriptions","organizations_url":"https://api.github.com/users/liqd/orgs","repos_url":"https://api.github.com/users/liqd/repos","events_url":"https://api.github.com/users/liqd/events{/privacy}","received_events_url":"https://api.github.com/users/liqd/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/liqd/thentos","description":"The swiss army knife of privacy-preserving identity management","fork":false,"url":"https://api.github.com/repos/liqd/thentos","forks_url":"https://api.github.com/repos/liqd/thentos/forks","keys_url":"https://api.github.com/repos/liqd/thentos/keys{/key_id}","collaborators_url":"https://api.github.com/repos/liqd/thentos/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/liqd/thentos/teams","hooks_url":"https://api.github.com/repos/liqd/thentos/hooks","issue_events_url":"https://api.github.com/repos/liqd/thentos/issues/events{/number}","events_url":"https://api.github.com/repos/liqd/thentos/events","assignees_url":"https://api.github.com/repos/liqd/thentos/assignees{/user}","branches_url":"https://api.github.com/repos/liqd/thentos/branches{/branch}","tags_url":"https://api.github.com/repos/liqd/thentos/tags","blobs_url":"https://api.github.com/repos/liqd/thentos/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/liqd/thentos/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/liqd/thentos/git/refs{/sha}","trees_url":"https://api.github.com/repos/liqd/thentos/git/trees{/sha}","statuses_url":"https://api.github.com/repos/liqd/thentos/statuses/{sha}","languages_url":"https://api.github.com/repos/liqd/thentos/languages","stargazers_url":"https://api.github.com/repos/liqd/thentos/stargazers","contributors_url":"https://api.github.com/repos/liqd/thentos/contributors","subscribers_url":"https://api.github.com/repos/liqd/thentos/subscribers","subscription_url":"https://api.github.com/repos/liqd/thentos/subscription","commits_url":"https://api.github.com/repos/liqd/thentos/commits{/sha}","git_commits_url":"https://api.github.com/repos/liqd/thentos/git/commits{/sha}","comments_url":"https://api.github.com/repos/liqd/thentos/comments{/number}","issue_comment_url":"https://api.github.com/repos/liqd/thentos/issues/comments{/number}","contents_url":"https://api.github.com/repos/liqd/thentos/contents/{+path}","compare_url":"https://api.github.com/repos/liqd/thentos/compare/{base}...{head}","merges_url":"https://api.github.com/repos/liqd/thentos/merges","archive_url":"https://api.github.com/repos/liqd/thentos/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/liqd/thentos/downloads","issues_url":"https://api.github.com/repos/liqd/thentos/issues{/number}","pulls_url":"https://api.github.com/repos/liqd/thentos/pulls{/number}","milestones_url":"https://api.github.com/repos/liqd/thentos/milestones{/number}","notifications_url":"https://api.github.com/repos/liqd/thentos/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/liqd/thentos/labels{/name}","releases_url":"https://api.github.com/repos/liqd/thentos/releases{/id}","created_at":"2015-02-27T14:31:39Z","updated_at":"2015-03-26T20:38:08Z","pushed_at":"2015-03-26T21:19:04Z","git_url":"git://github.com/liqd/thentos.git","ssh_url":"git@github.com:liqd/thentos.git","clone_url":"https://github.com/liqd/thentos.git","svn_url":"https://github.com/liqd/thentos","homepage":null,"size":7274,"stargazers_count":1,"watchers_count":1,"language":"Haskell","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":1,"mirror_url":null,"open_issues_count":4,"forks":1,"open_issues":4,"watchers":1,"default_branch":"master"}},"base":{"label":"liqd:master","ref":"master","sha":"aadafa065304f487b77741937e99a45389fdfb91","user":{"login":"liqd","id":2312088,"avatar_url":"https://avatars.githubusercontent.com/u/2312088?v=3","gravatar_id":"","url":"https://api.github.com/users/liqd","html_url":"https://github.com/liqd","followers_url":"https://api.github.com/users/liqd/followers","following_url":"https://api.github.com/users/liqd/following{/other_user}","gists_url":"https://api.github.com/users/liqd/gists{/gist_id}","starred_url":"https://api.github.com/users/liqd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/liqd/subscriptions","organizations_url":"https://api.github.com/users/liqd/orgs","repos_url":"https://api.github.com/users/liqd/repos","events_url":"https://api.github.com/users/liqd/events{/privacy}","received_events_url":"https://api.github.com/users/liqd/received_events","type":"Organization","site_admin":false},"repo":{"id":31423016,"name":"thentos","full_name":"liqd/thentos","owner":{"login":"liqd","id":2312088,"avatar_url":"https://avatars.githubusercontent.com/u/2312088?v=3","gravatar_id":"","url":"https://api.github.com/users/liqd","html_url":"https://github.com/liqd","followers_url":"https://api.github.com/users/liqd/followers","following_url":"https://api.github.com/users/liqd/following{/other_user}","gists_url":"https://api.github.com/users/liqd/gists{/gist_id}","starred_url":"https://api.github.com/users/liqd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/liqd/subscriptions","organizations_url":"https://api.github.com/users/liqd/orgs","repos_url":"https://api.github.com/users/liqd/repos","events_url":"https://api.github.com/users/liqd/events{/privacy}","received_events_url":"https://api.github.com/users/liqd/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/liqd/thentos","description":"The swiss army knife of privacy-preserving identity management","fork":false,"url":"https://api.github.com/repos/liqd/thentos","forks_url":"https://api.github.com/repos/liqd/thentos/forks","keys_url":"https://api.github.com/repos/liqd/thentos/keys{/key_id}","collaborators_url":"https://api.github.com/repos/liqd/thentos/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/liqd/thentos/teams","hooks_url":"https://api.github.com/repos/liqd/thentos/hooks","issue_events_url":"https://api.github.com/repos/liqd/thentos/issues/events{/number}","events_url":"https://api.github.com/repos/liqd/thentos/events","assignees_url":"https://api.github.com/repos/liqd/thentos/assignees{/user}","branches_url":"https://api.github.com/repos/liqd/thentos/branches{/branch}","tags_url":"https://api.github.com/repos/liqd/thentos/tags","blobs_url":"https://api.github.com/repos/liqd/thentos/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/liqd/thentos/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/liqd/thentos/git/refs{/sha}","trees_url":"https://api.github.com/repos/liqd/thentos/git/trees{/sha}","statuses_url":"https://api.github.com/repos/liqd/thentos/statuses/{sha}","languages_url":"https://api.github.com/repos/liqd/thentos/languages","stargazers_url":"https://api.github.com/repos/liqd/thentos/stargazers","contributors_url":"https://api.github.com/repos/liqd/thentos/contributors","subscribers_url":"https://api.github.com/repos/liqd/thentos/subscribers","subscription_url":"https://api.github.com/repos/liqd/thentos/subscription","commits_url":"https://api.github.com/repos/liqd/thentos/commits{/sha}","git_commits_url":"https://api.github.com/repos/liqd/thentos/git/commits{/sha}","comments_url":"https://api.github.com/repos/liqd/thentos/comments{/number}","issue_comment_url":"https://api.github.com/repos/liqd/thentos/issues/comments{/number}","contents_url":"https://api.github.com/repos/liqd/thentos/contents/{+path}","compare_url":"https://api.github.com/repos/liqd/thentos/compare/{base}...{head}","merges_url":"https://api.github.com/repos/liqd/thentos/merges","archive_url":"https://api.github.com/repos/liqd/thentos/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/liqd/thentos/downloads","issues_url":"https://api.github.com/repos/liqd/thentos/issues{/number}","pulls_url":"https://api.github.com/repos/liqd/thentos/pulls{/number}","milestones_url":"https://api.github.com/repos/liqd/thentos/milestones{/number}","notifications_url":"https://api.github.com/repos/liqd/thentos/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/liqd/thentos/labels{/name}","releases_url":"https://api.github.com/repos/liqd/thentos/releases{/id}","created_at":"2015-02-27T14:31:39Z","updated_at":"2015-03-26T20:38:08Z","pushed_at":"2015-03-26T21:19:04Z","git_url":"git://github.com/liqd/thentos.git","ssh_url":"git@github.com:liqd/thentos.git","clone_url":"https://github.com/liqd/thentos.git","svn_url":"https://github.com/liqd/thentos","homepage":null,"size":7274,"stargazers_count":1,"watchers_count":1,"language":"Haskell","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":1,"mirror_url":null,"open_issues_count":4,"forks":1,"open_issues":4,"watchers":1,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/liqd/thentos/pulls/18"},"html":{"href":"https://github.com/liqd/thentos/pull/18"},"issue":{"href":"https://api.github.com/repos/liqd/thentos/issues/18"},"comments":{"href":"https://api.github.com/repos/liqd/thentos/issues/18/comments"},"review_comments":{"href":"https://api.github.com/repos/liqd/thentos/pulls/18/comments"},"review_comment":{"href":"https://api.github.com/repos/liqd/thentos/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/liqd/thentos/pulls/18/commits"},"statuses":{"href":"https://api.github.com/repos/liqd/thentos/statuses/9023c4730b12ff8c4979ed33cb626de97dbb80d5"}}}}