From 51d034042ebb5e2505d42d7889107878fb5fbb2e Mon Sep 17 00:00:00 2001 From: Alex Brandt Date: Thu, 11 Jan 2018 21:31:19 -0600 Subject: [PATCH] add first draft of cabal parsing service --- .dockerignore | 26 ++++++ .gitignore | 25 ++++++ .travis.yml | 88 +++++++++++++++++++ CODE_OF_CONDUCT.md | 74 ++++++++++++++++ Dockerfile | 25 ++++++ README.md | 56 ++++++++++++ cabal-parser.cabal | 61 ++++++++++--- cabal-parser.nix | 21 +++++ cabal.nix | 21 +++++ default.nix | 12 +++ docker-compose.yml | 15 ++++ shell.nix | 1 + src/API.hs | 26 ++++++ src/Dependency.hs | 63 +++++++++++++ .../Types/PackageDescription/MimeUnrender.hs | 17 ++++ src/Distribution/Types/PackageName/JSON.hs | 10 +++ src/Distribution/Version/JSON.hs | 10 +++ src/Environment.hs | 16 ++++ src/Main.hs | 14 ++- 19 files changed, 566 insertions(+), 15 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 cabal-parser.nix create mode 100644 cabal.nix create mode 100644 default.nix create mode 100644 docker-compose.yml create mode 100644 shell.nix create mode 100644 src/API.hs create mode 100644 src/Dependency.hs create mode 100644 src/Distribution/Types/PackageDescription/MimeUnrender.hs create mode 100644 src/Distribution/Types/PackageName/JSON.hs create mode 100644 src/Distribution/Version/JSON.hs create mode 100644 src/Environment.hs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dd39283 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +# Created by https://www.gitignore.io/api/haskell + +### Haskell ### +dist +!dist/build/cabal-parser/cabal-parser +dist-* +cabal-dev +*.o +*.hi +*.chi +*.chs.h +*.dyn_o +*.dyn_hi +.hpc +.hsenv +.cabal-sandbox/ +cabal.sandbox.config +*.prof +*.aux +*.hp +*.eventlog +.stack-work/ +cabal.project.local +.HTF/ + +# End of https://www.gitignore.io/api/haskell diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d910ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Created by https://www.gitignore.io/api/haskell + +### Haskell ### +dist +dist-* +cabal-dev +*.o +*.hi +*.chi +*.chs.h +*.dyn_o +*.dyn_hi +.hpc +.hsenv +.cabal-sandbox/ +cabal.sandbox.config +*.prof +*.aux +*.hp +*.eventlog +.stack-work/ +cabal.project.local +.HTF/ + +# End of https://www.gitignore.io/api/haskell diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..66dcb0c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,88 @@ +# This Travis job script has been generated by a script via +# +# make_travis_yml_2.hs 'cabal-parser.cabal' +# +# For more information, see https://github.com/hvr/multi-ghc-travis +# +language: c +sudo: false + +git: + submodules: false # whether to recursively clone submodules + +cache: + directories: + - $HOME/.cabal/packages + - $HOME/.cabal/store + +before_cache: + - rm -fv $HOME/.cabal/packages/hackage.haskell.org/build-reports.log + # remove files that are regenerated by 'cabal update' + - rm -fv $HOME/.cabal/packages/hackage.haskell.org/00-index.* + - rm -fv $HOME/.cabal/packages/hackage.haskell.org/*.json + - rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.cache + - rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.tar + - rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.tar.idx + +matrix: + include: + - compiler: "ghc-8.0.1" + # env: TEST=--disable-tests BENCH=--disable-benchmarks + addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.0.1], sources: [hvr-ghc]}} + - compiler: "ghc-8.0.2" + # env: TEST=--disable-tests BENCH=--disable-benchmarks + addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.0.2], sources: [hvr-ghc]}} + - compiler: "ghc-8.2.1" + # env: TEST=--disable-tests BENCH=--disable-benchmarks + addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.2.1], sources: [hvr-ghc]}} + - compiler: "ghc-head" + # env: TEST=--disable-tests BENCH=--disable-benchmarks + addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-head], sources: [hvr-ghc]}} + + allow_failures: + - compiler: "ghc-head" + +before_install: + - HC=${CC} + - unset CC + - PATH=/opt/ghc/bin:/opt/ghc-ppa-tools/bin:$PATH + - PKGNAME='cabal-parser' + +install: + - cabal --version + - echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]" + - BENCH=${BENCH---enable-benchmarks} + - TEST=${TEST---enable-tests} + - travis_retry cabal update -v + - sed -i 's/^jobs:/-- jobs:/' ${HOME}/.cabal/config + - rm -fv cabal.project.local + - "echo 'packages: .' > cabal.project" + - rm -f cabal.project.freeze + - cabal new-build -w ${HC} ${TEST} ${BENCH} --dep -j2 all + - cabal new-build -w ${HC} --disable-tests --disable-benchmarks --dep -j2 all + +# Here starts the actual work to be performed for the package under test; +# any command which exits with a non-zero exit code causes the build to fail. +script: + - if [ -f configure.ac ]; then autoreconf -i; fi + - rm -rf .ghc.environment.* dist/ + - cabal sdist # test that a source-distribution can be generated + - cd dist/ + - SRCTAR=(${PKGNAME}-*.tar.gz) + - SRC_BASENAME="${SRCTAR/%.tar.gz}" + - tar -xvf "./$SRC_BASENAME.tar.gz" + - cd "$SRC_BASENAME/" +## from here on, CWD is inside the extracted source-tarball + - rm -fv cabal.project.local + - "echo 'packages: .' > cabal.project" + # this builds all libraries and executables (without tests/benchmarks) + - rm -f cabal.project.freeze + - cabal new-build -w ${HC} --disable-tests --disable-benchmarks all + # this builds all libraries and executables (including tests/benchmarks) + # - rm -rf ./dist-newstyle + + # build & run tests + - cabal new-build -w ${HC} ${TEST} ${BENCH} all + - if [ "x$TEST" = "x--enable-tests" ]; then cabal new-test -w ${HC} ${TEST} all; fi + +# EOF diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..27e9cc6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at andrew@libraries.io. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7646fbf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM alpine:3.7 +MAINTAINER Alex Brandt + +RUN apk add --no-cache musl-dev zlib-dev +RUN apk add --no-cache cabal ghc + +RUN cabal update + +WORKDIR /usr/local/src/cabal-parser + +COPY ./cabal-parser.cabal /usr/local/src/cabal-parser/cabal-parser.cabal +RUN cabal install -j --only-dependencies + +COPY . /usr/local/src/cabal-parser +RUN cabal build -j --ghc-options="-static -optc-static -optl-static -optl-pthread" + +FROM alpine:3.7 +MAINTAINER Alex Brandt + +RUN apk add --no-cache ca-certificates + +COPY --from=0 /usr/local/src/cabal-parser/dist/build/cabal-parser/cabal-parser / + +ENTRYPOINT [ "/cabal-parser" ] +CMD [ "Heroku Compatability" ] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e923fcd --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Description + +Tiny Web Service for Parsing Cabal Files + +Helper service for bibliothecary to parse cabal files from various sources. +Provides a fiat JSON represenation for a given cabal file posted to this HTTP API. + +# Getting Started + +Developer documentation can be generated with: + +```bash +cabal haddock --executables +``` + +Once the documentation is generated, it is available at: +`./dist/doc/html/cabal-parser/cabal-parser/index.html`. + +## Locally with [`docker-compose`][docker-compose] + +This project is setup to run with [`docker-compose`][docker-compose]. Running +the following command will build a [docker] image (includes building +cabal-parser), and start all requisite services as [docker] containers. + +```bash +docker-compose up -d +``` + +cabal-parser will be available at once this +command finishes executing. You can get `PORT` from `docker ps`. + +## Locally with [`nix-shell`][nix-shell] + +This project is setup with [`nix-shell`][nix-shell]. Running the following +command will build a local development environment where all of the +supplementary tools are pre-installed. + +```bash +nix-env -i cabal +nix-shell +``` + +Once this command finishes executing, the libraries and other tools are +available. You will still need to have cabal installed another way. + +## Others + +This project utilizes [`cabal`][cabal] like most [Haskell] projects and the +standard [Haskell] development environment for your platform should work just +fine. + +[cabal]: https://www.haskell.org/cabal/ +[docker-compose]: https://docs.docker.com/compose/ +[docker]: https://docs.docker.com/ +[Haskell]: https://www.haskell.org/ +[nix-shell]: https://nixos.org/nix/manual/#sec-nix-shell diff --git a/cabal-parser.cabal b/cabal-parser.cabal index ae4bd22..ea8ee37 100644 --- a/cabal-parser.cabal +++ b/cabal-parser.cabal @@ -1,25 +1,60 @@ --- Initial cabal-parser.cabal generated by cabal init. For further --- documentation, see http://haskell.org/cabal/users-guide/ - name: cabal-parser version: 0.1.0.0 -synopsis: Cabal Parser for Libraries.io's bibliothecary --- description: +synopsis: Tiny Web Service for Parsing Cabal Files + +description: + Helper service for bibliothecary to parse cabal files from various sources. + Provides a fiat JSON represenation for a given cabal file posted to this HTTP API. + homepage: https://github.com/alunduil/librariesio-cabal-parser +bug-reports: https://github.com/alunduil/librariesio-cabal-parser/issues license: GPL-3 license-file: LICENSE author: Alex Brandt maintainer: alunduil@alunduil.com --- copyright: +copyright: (c) 2018 Alex Brandt category: Development build-type: Simple -extra-source-files: ChangeLog.md -cabal-version: >=1.10 +cabal-version: >= 1.10 +tested-with: GHC == 8.* + +extra-source-files: + ChangeLog.md + , LICENSE + , README.md + , Setup.hs + +source-repository head + type: git + location: https://github.com/alunduil/librariesio-cabal-parser + branch: develop executable cabal-parser - main-is: Main.hs - -- other-modules: - -- other-extensions: - build-depends: base >=4.9 && <4.10 - hs-source-dirs: src default-language: Haskell2010 + main-is: Main.hs + + ghc-options: -Wall -fwarn-tabs -fwarn-monomorphism-restriction + -fwarn-unused-do-bind + + hs-source-dirs: + src + + other-modules: + API + , Dependency + , Distribution.Types.PackageDescription.MimeUnrender + , Distribution.Types.PackageName.JSON + , Distribution.Version.JSON + , Environment + + build-depends: + aeson == 1.1.* + , base >= 4.9 && < 4.10 + , Cabal == 2.0.* + , envy == 1.3.* + , servant-server == 0.11.* + , text == 1.2.* + , utf8-string == 1.0.* + , warp == 3.2.* + + other-extensions: diff --git a/cabal-parser.nix b/cabal-parser.nix new file mode 100644 index 0000000..aa77ddc --- /dev/null +++ b/cabal-parser.nix @@ -0,0 +1,21 @@ +let + config = { + packageOverrides = pkgs: rec { + haskellPackages = pkgs.haskellPackages.override { + overrides = haskellPackagesNew: haskellPackagesOld: rec { + + Cabal = + haskellPackagesNew.callPackage ./cabal.nix { }; + + cabal-parser = + haskellPackagesNew.callPackage ./default.nix { }; + + }; + }; + }; + }; + + pkgs = import { inherit config; }; +in + { cabal-parser = pkgs.haskellPackages.cabal-parser; + } diff --git a/cabal.nix b/cabal.nix new file mode 100644 index 0000000..cd7b86a --- /dev/null +++ b/cabal.nix @@ -0,0 +1,21 @@ +{ mkDerivation, array, base, binary, bytestring, containers +, deepseq, directory, filepath, pretty, process, QuickCheck, stdenv +, tagged, tar, tasty, tasty-hunit, tasty-quickcheck, time, unix +}: +mkDerivation { + pname = "Cabal"; + version = "2.0.0.2"; + sha256 = "0chhl2113jbahd5gychx9rdqj1aw22h7dyj6z44871hzqxngx6bc"; + libraryHaskellDepends = [ + array base binary bytestring containers deepseq directory filepath + pretty process time unix + ]; + testHaskellDepends = [ + array base bytestring containers directory filepath pretty + QuickCheck tagged tar tasty tasty-hunit tasty-quickcheck + ]; + doCheck = false; + homepage = "http://www.haskell.org/cabal/"; + description = "A framework for packaging Haskell software"; + license = stdenv.lib.licenses.bsd3; +} diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..b5f35a4 --- /dev/null +++ b/default.nix @@ -0,0 +1,12 @@ +{ mkDerivation, base, Cabal, envy, servant-server, stdenv }: +mkDerivation { + pname = "cabal-parser"; + version = "0.1.0.0"; + src = ./.; + isLibrary = false; + isExecutable = true; + executableHaskellDepends = [ base Cabal envy servant-server ]; + homepage = "https://github.com/alunduil/librariesio-cabal-parser"; + description = "Tiny Web Service for Parsing Cabal Files"; + license = stdenv.lib.licenses.gpl3; +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c596a0c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3' + +services: + cabal-parser: + build: + context: . + dockerfile: Dockerfile + image: cabal-parser:latest + networks: + - cabal-parser + ports: + - "5000" + +networks: + cabal-parser: diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..cdf55d2 --- /dev/null +++ b/shell.nix @@ -0,0 +1 @@ +(import ./cabal-parser.nix).cabal-parser.env diff --git a/src/API.hs b/src/API.hs new file mode 100644 index 0000000..e9b9b36 --- /dev/null +++ b/src/API.hs @@ -0,0 +1,26 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeOperators #-} + +module API + ( API + , server + ) where + +import Control.Monad.IO.Class (liftIO) +import Data.Aeson (encode) +import Distribution.Types.PackageDescription (PackageDescription) +import Servant ((:>), JSON, PlainText, Post, ReqBody, Server) +import System.IO (hFlush, stdout) + +import Dependency + +import Distribution.Types.PackageDescription.MimeUnrender () + +type API = "parse" :> ReqBody '[PlainText] PackageDescription :> Post '[JSON] [Dependency] + +server :: Server API +server d = + do liftIO $ print d >> hFlush stdout + liftIO $ print (encode ds) >> hFlush stdout + return ds + where ds = dependencies d diff --git a/src/Dependency.hs b/src/Dependency.hs new file mode 100644 index 0000000..5733d6e --- /dev/null +++ b/src/Dependency.hs @@ -0,0 +1,63 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module Dependency + ( Dependency + , dependencies + ) where + +import Data.Aeson ((.=), object, Value (String), ToJSON (toJSON)) +import Distribution.Simple.BuildToolDepends (getAllToolDependencies) +import Distribution.Types.Benchmark (Benchmark (benchmarkBuildInfo)) +import Distribution.Types.BuildInfo (BuildInfo (targetBuildDepends)) +import Distribution.Types.Executable (Executable (buildInfo)) +import Distribution.Types.Library (Library (libBuildInfo)) +import Distribution.Types.PackageDescription (allLibraries, PackageDescription (benchmarks, executables, testSuites)) +import Distribution.Types.PackageName (PackageName) +import Distribution.Types.TestSuite (TestSuite (testBuildInfo)) +import Distribution.Version (VersionRange) + +import qualified Distribution.Types.Dependency as Cabal (Dependency (Dependency)) +import qualified Distribution.Types.ExeDependency as Cabal (ExeDependency (ExeDependency)) + +import Distribution.Types.PackageName.JSON () +import Distribution.Version.JSON () + +data Dependency = Dependency + { dName :: PackageName + , dRequirement :: VersionRange + , dType :: DependencyType + } + +instance ToJSON Dependency where + toJSON Dependency{..} = object + [ "name" .= dName + , "requirement" .= dRequirement + , "type" .= dType + ] + +data DependencyType = Build + | Runtime + | Test + | Benchmark + +instance ToJSON DependencyType where + toJSON Build = String "build" + toJSON Runtime = String "runtime" + toJSON Test = String "test" + toJSON Benchmark = String "benchmark" + +dependencies :: PackageDescription -> [Dependency] +dependencies d = (dependencies' Runtime <$> libBuildInfo =<< allLibraries d) ++ + (dependencies' Runtime <$> buildInfo =<< executables d) ++ + (dependencies' Test <$> testBuildInfo =<< testSuites d) ++ + (dependencies' Benchmark <$> benchmarkBuildInfo =<< benchmarks d) + + where dependencies' t b = (fromCabalExeDependency <$> getAllToolDependencies d b) ++ + (fromCabalDependency t <$> targetBuildDepends b) + +fromCabalExeDependency :: Cabal.ExeDependency -> Dependency +fromCabalExeDependency (Cabal.ExeDependency n _ v) = Dependency n v Build + +fromCabalDependency :: DependencyType -> Cabal.Dependency -> Dependency +fromCabalDependency t (Cabal.Dependency n v) = Dependency n v t diff --git a/src/Distribution/Types/PackageDescription/MimeUnrender.hs b/src/Distribution/Types/PackageDescription/MimeUnrender.hs new file mode 100644 index 0000000..386a6ff --- /dev/null +++ b/src/Distribution/Types/PackageDescription/MimeUnrender.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} + +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Distribution.Types.PackageDescription.MimeUnrender where + +import Data.ByteString.Lazy.UTF8 (toString) +import Distribution.PackageDescription.Parse (parseGenericPackageDescription, ParseResult (ParseFailed, ParseOk)) +import Distribution.Types.GenericPackageDescription (packageDescription) +import Distribution.Types.PackageDescription (PackageDescription) +import Servant (MimeUnrender (mimeUnrender), PlainText) + +instance MimeUnrender PlainText PackageDescription where + mimeUnrender _ = toEither . parseGenericPackageDescription . toString + where toEither (ParseFailed e) = Left $ show e + toEither (ParseOk _ d) = Right $ packageDescription d diff --git a/src/Distribution/Types/PackageName/JSON.hs b/src/Distribution/Types/PackageName/JSON.hs new file mode 100644 index 0000000..abfeeb3 --- /dev/null +++ b/src/Distribution/Types/PackageName/JSON.hs @@ -0,0 +1,10 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Distribution.Types.PackageName.JSON where + +import Data.Aeson (ToJSON (toJSON), Value (String)) +import Data.Text (pack) +import Distribution.Types.PackageName (PackageName, unPackageName) + +instance ToJSON PackageName where + toJSON = String . pack . unPackageName diff --git a/src/Distribution/Version/JSON.hs b/src/Distribution/Version/JSON.hs new file mode 100644 index 0000000..fb5b322 --- /dev/null +++ b/src/Distribution/Version/JSON.hs @@ -0,0 +1,10 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Distribution.Version.JSON where + +import Data.Aeson (ToJSON (toJSON), Value (String)) +import Data.Text (pack) +import Distribution.Version (simplifyVersionRange, VersionRange) + +instance ToJSON VersionRange where + toJSON = String . pack . show . simplifyVersionRange diff --git a/src/Environment.hs b/src/Environment.hs new file mode 100644 index 0000000..fd74a50 --- /dev/null +++ b/src/Environment.hs @@ -0,0 +1,16 @@ +module Environment + ( Environment + ( Environment + , port + ) + ) where + +import System.Envy ((.!=), envMaybe, FromEnv (fromEnv)) + +newtype Environment = Environment { port :: Int } + +instance Show Environment where + show e = "PORT=" ++ show (port e) ++ "\n" + +instance FromEnv Environment where + fromEnv = Environment <$> envMaybe "PORT" .!= 5000 diff --git a/src/Main.hs b/src/Main.hs index 65ae4a0..8605872 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,4 +1,14 @@ -module Main where +module Main (main) where + +import Network.Wai.Handler.Warp (run) +import Servant (Application, Proxy (Proxy), serve) +import System.Envy (decodeEnv) + +import API +import Environment main :: IO () -main = putStrLn "Hello, Haskell!" +main = either fail (flip run application . port) =<< decodeEnv + +application :: Application +application = serve (Proxy :: Proxy API) server