From 94e07f951916f28256ab64f05b75c2f12e53dabc Mon Sep 17 00:00:00 2001 From: Nickolay Kudasov Date: Mon, 18 Jan 2016 14:45:25 +0300 Subject: [PATCH 01/50] Add basic configuration for Read The Docs (based on Stack's) --- doc/CONTRIBUTING.md | 1 + doc/README.md | 1 + doc/conf.py | 288 ++++++++++++++++++++++++++++++++++++++++++++ doc/index.rst | 11 ++ 4 files changed, 301 insertions(+) create mode 120000 doc/CONTRIBUTING.md create mode 120000 doc/README.md create mode 100644 doc/conf.py create mode 100644 doc/index.rst diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md new file mode 120000 index 00000000..44fcc634 --- /dev/null +++ b/doc/CONTRIBUTING.md @@ -0,0 +1 @@ +../CONTRIBUTING.md \ No newline at end of file diff --git a/doc/README.md b/doc/README.md new file mode 120000 index 00000000..32d46ee8 --- /dev/null +++ b/doc/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 00000000..c2761418 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +# +# servant documentation build configuration file, created by +# sphinx-quickstart on Mon Nov 23 13:24:36 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +import shlex +from recommonmark.parser import CommonMarkParser + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +source_suffix = ['.md', '.rst'] + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'servant' +copyright = u'2015-2016, Servant contributors' +author = u'Servant contributors' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +# version = 'latest' +# The full version, including alpha/beta/rc tags. +# release = 'latest' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'servantdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'servant.tex', u'servant Documentation', + u'Servant contributors', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'servant', u'servant Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'servant', u'servant Documentation', + author, 'servant', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + +source_parsers = { + '.md': CommonMarkParser, +} diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 00000000..41b48c52 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,11 @@ +servant – Type-Level Web DSL +============================ + +Documentation table of contents +------------------------------- + +.. toctree:: + + README.md + CONTRIBUTING.md + From 3b3c929b408a56e4cba4ca34d5a2dc9fa1486751 Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Mon, 25 Jan 2016 14:11:40 +0100 Subject: [PATCH 02/50] Move tutorial files over --- .gitignore | 1 + tutorial/Makefile | 216 +++++++ tutorial/api-type.lhs | 309 ++++++++++ tutorial/client.lhs | 138 +++++ tutorial/conf.py | 287 +++++++++ tutorial/docs.lhs | 227 +++++++ tutorial/index.rst | 68 +++ tutorial/javascript.lhs | 175 ++++++ tutorial/requirements.txt | 25 + tutorial/server.lhs | 1176 +++++++++++++++++++++++++++++++++++++ 10 files changed, 2622 insertions(+) create mode 100644 tutorial/Makefile create mode 100644 tutorial/api-type.lhs create mode 100644 tutorial/client.lhs create mode 100644 tutorial/conf.py create mode 100644 tutorial/docs.lhs create mode 100644 tutorial/index.rst create mode 100644 tutorial/javascript.lhs create mode 100644 tutorial/requirements.txt create mode 100644 tutorial/server.lhs diff --git a/.gitignore b/.gitignore index 2b2f3487..ee3cbf51 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ Setup .stack-work shell.nix default.nix +tutorial/_build diff --git a/tutorial/Makefile b/tutorial/Makefile new file mode 100644 index 00000000..95957c1a --- /dev/null +++ b/tutorial/Makefile @@ -0,0 +1,216 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/generics-eot.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/generics-eot.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/generics-eot" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/generics-eot" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/tutorial/api-type.lhs b/tutorial/api-type.lhs new file mode 100644 index 00000000..71c84631 --- /dev/null +++ b/tutorial/api-type.lhs @@ -0,0 +1,309 @@ +--- +title: A web API as a type +toc: true +--- + +The source for this tutorial section is a literate haskell file, so first we +need to have some language extensions and imports: + +> {-# LANGUAGE DataKinds #-} +> {-# LANGUAGE TypeOperators #-} +> +> module ApiType where +> +> import Data.Text +> import Servant.API + +Consider the following informal specification of an API: + + > The endpoint at `/users` expects a GET request with query string parameter + > `sortby` whose value can be one of `age` or `name` and returns a + > list/array of JSON objects describing users, with fields `age`, `name`, + > `email`, `registration_date`". + +You *should* be able to formalize that. And then use the formalized version to +get you much of the way towards writing a web app. And all the way towards +getting some client libraries, and documentation (and in the future, who knows +- tests, HATEOAS, ...). + +How would we describe it with servant? As mentioned earlier, an endpoint +description is a good old Haskell **type**: + +> type UserAPI = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User] +> +> data SortBy = Age | Name +> +> data User = User { +> name :: String, +> age :: Int +> } + +Let's break that down: + +- `"users"` says that our endpoint will be accessible under `/users`; +- `QueryParam "sortby" SortBy`, where `SortBy` is defined by `data SortBy = Age +| Name`, says that the endpoint has a query string parameter named `sortby` +whose value will be extracted as a value of type `SortBy`. +- `Get '[JSON] [User]` says that the endpoint will be accessible through HTTP +GET requests, returning a list of users encoded as JSON. You will see +later how you can make use of this to make your data available under different +formats, the choice being made depending on the [Accept +header](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html) specified in +the client's request. +- the `:>` operator that separates the various "combinators" just lets you +sequence static path fragments, URL captures and other combinators. The +ordering only matters for static path fragments and URL captures. `"users" :> +"list-all" :> Get '[JSON] [User]`, equivalent to `/users/list-all`, is +obviously not the same as `"list-all" :> "users" :> Get '[JSON] [User]`, which +is equivalent to `/list-all/users`. This means that sometimes `:>` is somehow +equivalent to `/`, but sometimes it just lets you chain another combinator. + +We can also describe APIs with multiple endpoints by using the `:<|>` +combinators. Here's an example: + +> type UserAPI2 = "users" :> "list-all" :> Get '[JSON] [User] +> :<|> "list-all" :> "users" :> Get '[JSON] [User] + +*servant* provides a fair amount of combinators out-of-the-box, but you can +always write your own when you need it. Here's a quick overview of all the +combinators that servant comes with. + +Combinators +=========== + +Static strings +-------------- + +As you've already seen, you can use type-level strings (enabled with the +`DataKinds` language extension) for static path fragments. Chaining +them amounts to `/`-separating them in a URL. + +> type UserAPI3 = "users" :> "list-all" :> "now" :> Get '[JSON] [User] +> -- describes an endpoint reachable at: +> -- /users/list-all/now + +`Delete`, `Get`, `Patch`, `Post` and `Put` +------------------------------------------ + +These 5 combinators are very similar except that they each describe a +different HTTP method. This is how they're declared + +``` haskell +data Delete (contentTypes :: [*]) a +data Get (contentTypes :: [*]) a +data Patch (contentTypes :: [*]) a +data Post (contentTypes :: [*]) a +data Put (contentTypes :: [*]) a +``` + +An endpoint ends with one of the 5 combinators above (unless you write your +own). Examples: + +> type UserAPI4 = "users" :> Get '[JSON] [User] +> :<|> "admins" :> Get '[JSON] [User] + +`Capture` +--------- + +URL captures are parts of the URL that are variable and whose actual value is +captured and passed to the request handlers. In many web frameworks, you'll see +it written as in `/users/:userid`, with that leading `:` denoting that `userid` +is just some kind of variable name or placeholder. For instance, if `userid` is +supposed to range over all integers greater or equal to 1, our endpoint will +match requests made to `/users/1`, `/users/143` and so on. + +The `Capture` combinator in servant takes a (type-level) string representing +the "name of the variable" and a type, which indicates the type we want to +decode the "captured value" to. + +``` haskell +data Capture (s :: Symbol) a +-- s :: Symbol just says that 's' must be a type-level string. +``` + +In some web frameworks, you use regexes for captures. We use a +[`FromText`](https://hackage.haskell.org/package/servant/docs/Servant-Common-Text.html#t:FromText) +class, which the captured value must be an instance of. + +Examples: + +> type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User +> -- equivalent to 'GET /user/:userid' +> -- except that we explicitly say that "userid" +> -- must be an integer +> +> :<|> "user" :> Capture "userid" Integer :> Delete '[] () +> -- equivalent to 'DELETE /user/:userid' + +`QueryParam`, `QueryParams`, `QueryFlag`, `MatrixParam`, `MatrixParams` and `MatrixFlag` +---------------------------------------------------------------------------------------- + +`QueryParam`, `QueryParams` and `QueryFlag` are about query string +parameters, i.e., those parameters that come after the question mark +(`?`) in URLs, like `sortby` in `/users?sortby=age`, whose value is +set to `age`. `QueryParams` lets you specify that the query parameter +is actually a list of values, which can be specified using +`?param[]=value1¶m[]=value2`. This represents a list of values +composed of `value1` and `value2`. `QueryFlag` lets you specify a +boolean-like query parameter where a client isn't forced to specify a +value. The absence or presence of the parameter's name in the query +string determines whether the parameter is considered to have the +value `True` or `False`. For instance, `/users?active` would list only +active users whereas `/users` would list them all. + +Here are the corresponding data type declarations: + +``` haskell +data QueryParam (sym :: Symbol) a +data QueryParams (sym :: Symbol) a +data QueryFlag (sym :: Symbol) +``` + +[Matrix parameters](http://www.w3.org/DesignIssues/MatrixURIs.html) +are similar to query string parameters, but they can appear anywhere +in the paths (click the link for more details). A URL with matrix +parameters in it looks like `/users;sortby=age`, as opposed to +`/users?sortby=age` with query string parameters. The big advantage is +that they are not necessarily at the end of the URL. You could have +`/users;active=true;registered_after=2005-01-01/locations` to get +geolocation data about users whom are still active and registered +after *January 1st, 2005*. + +Corresponding data type declarations below. + +``` haskell +data MatrixParam (sym :: Symbol) a +data MatrixParams (sym :: Symbol) a +data MatrixFlag (sym :: Symbol) +``` + +Examples: + +> type UserAPI6 = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User] +> -- equivalent to 'GET /users?sortby={age, name}' +> +> :<|> "users" :> MatrixParam "sortby" SortBy :> Get '[JSON] [User] +> -- equivalent to 'GET /users;sortby={age, name}' + +Again, your handlers don't have to deserialize these things (into, for example, +a `SortBy`). *servant* takes care of it. + +`ReqBody` +--------- + +Each HTTP request can carry some additional data that the server can use in its +*body*, and this data can be encoded in any format -- as long as the server +understands it. This can be used for example for an endpoint for creating new +users: instead of passing each field of the user as a separate query string +parameter or something dirty like that, we can group all the data into a JSON +object. This has the advantage of supporting nested objects. + +*servant*'s `ReqBody` combinator takes a list of content types in which the +data encoded in the request body can be represented and the type of that data. +And, as you might have guessed, you don't have to check the content-type +header, and do the deserialization yourself. We do it for you. And return `Bad +Request` or `Unsupported Content Type` as appropriate. + +Here's the data type declaration for it: + +``` haskell +data ReqBody (contentTypes :: [*]) a +``` + +Examples: + +> type UserAPI7 = "users" :> ReqBody '[JSON] User :> Post '[JSON] User +> -- - equivalent to 'POST /users' with a JSON object +> -- describing a User in the request body +> -- - returns a User encoded in JSON +> +> :<|> "users" :> Capture "userid" Integer +> :> ReqBody '[JSON] User +> :> Put '[JSON] User +> -- - equivalent to 'PUT /users/:userid' with a JSON +> -- object describing a User in the request body +> -- - returns a User encoded in JSON + +Request `Header`s +----------------- + +Request headers are used for various purposes, from caching to carrying +auth-related data. They consist of a header name and an associated value. An +example would be `Accept: application/json`. + +The `Header` combinator in servant takes a type-level string for the header +name and the type to which we want to decode the header's value (from some +textual representation), as illustrated below: + +``` haskell +data Header (sym :: Symbol) a +``` + +Here's an example where we declare that an endpoint makes use of the +`User-Agent` header which specifies the name of the software/library used by +the client to send the request. + +> type UserAPI8 = "users" :> Header "User-Agent" Text :> Get '[JSON] [User] + +Content types +------------- + +So far, whenever we have used a combinator that carries a list of content +types, we've always specified `'[JSON]`. However, *servant* lets you use several +content types, and also lets you define your own content types. + +Four content-types are provided out-of-the-box by the core *servant* package: +`JSON`, `PlainText`, `FormUrlEncoded` and `OctetStream`. If for some obscure +reason you wanted one of your endpoints to make your user data available under +those 4 formats, you would write the API type as below: + +> type UserAPI9 = "users" :> Get '[JSON, PlainText, FormUrlEncoded, OctetStream] [User] + +We also provide an HTML content-type, but since there's no single library +that everyone uses, we decided to release 2 packages, *servant-lucid* and +*servant-blaze*, to provide HTML encoding of your data. + +We will further explain how these content types and your data types can play +together in the [section about serving an API](/tutorial/server.html). + +Response `Headers` +------------------ + +Just like an HTTP request, the response generated by a webserver can carry +headers too. *servant* provides a `Headers` combinator that carries a list of +`Header` and can be used by simply wrapping the "return type" of an endpoint +with it. + +``` haskell +data Headers (ls :: [*]) a +``` + +If you want to describe an endpoint that returns a "User-Count" header in each +response, you could write it as below: + +> type UserAPI10 = "users" :> Get '[JSON] (Headers '[Header "User-Count" Integer] [User]) + +Interoperability with other WAI `Application`s: `Raw` +----------------------------------------------------- + +Finally, we also include a combinator named `Raw` that can be used for two reasons: + +- You want to serve static files from a given directory. In that case you can just say: + +> type UserAPI11 = "users" :> Get '[JSON] [User] +> -- a /users endpoint +> +> :<|> Raw +> -- requests to anything else than /users +> -- go here, where the server will try to +> -- find a file with the right name +> -- at the right path + +- You more generally want to plug a [WAI `Application`](http://hackage.haskell.org/package/wai) +into your webservice. Static file serving is a specific example of that. The API type would look the +same as above though. (You can even combine *servant* with other web frameworks +this way!) + + diff --git a/tutorial/client.lhs b/tutorial/client.lhs new file mode 100644 index 00000000..21779f2b --- /dev/null +++ b/tutorial/client.lhs @@ -0,0 +1,138 @@ +--- +title: Deriving Haskell functions to query an API +toc: true +--- + +While defining handlers that serve an API has a lot to it, querying an API is simpler: we do not care about what happens inside the webserver, we just need to know how to talk to it and get a response back. Except that we usually have to write the querying functions by hand because the structure of the API isn't a first class citizen and can't be inspected to generate a bunch of client-side functions. + +*servant* however has a way to inspect API, because APIs are just Haskell types and (GHC) Haskell lets us do quite a few things with types. In the same way that we look at an API type to deduce the types the handlers should have, we can inspect the structure of the API to *derive* Haskell functions that take one argument for each occurence of `Capture`, `ReqBody`, `QueryParam` +and friends. By *derive*, we mean that there's no code generation involved, the functions are defined just by the structure of the API type. + +The source for this tutorial section is a literate haskell file, so first we +need to have some language extensions and imports: + +> {-# LANGUAGE DataKinds #-} +> {-# LANGUAGE DeriveGeneric #-} +> {-# LANGUAGE TypeOperators #-} +> +> module Client where +> +> import Control.Monad.Trans.Either +> import Data.Aeson +> import Data.Proxy +> import GHC.Generics +> import Servant.API +> import Servant.Client + +Also, we need examples for some domain specific data types: + +> data Position = Position +> { x :: Int +> , y :: Int +> } deriving (Show, Generic) +> +> instance FromJSON Position +> +> newtype HelloMessage = HelloMessage { msg :: String } +> deriving (Show, Generic) +> +> instance FromJSON HelloMessage +> +> data ClientInfo = ClientInfo +> { clientName :: String +> , clientEmail :: String +> , clientAge :: Int +> , clientInterestedIn :: [String] +> } deriving Generic +> +> instance ToJSON ClientInfo +> +> data Email = Email +> { from :: String +> , to :: String +> , subject :: String +> , body :: String +> } deriving (Show, Generic) +> +> instance FromJSON Email + +Enough chitchat, let's see an example. Consider the following API type from the previous section: + +> type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position +> :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage +> :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email + +What we are going to get with *servant-client* here is 3 functions, one to query each endpoint: + +> position :: Int -- ^ value for "x" +> -> Int -- ^ value for "y" +> -> EitherT ServantError IO Position +> +> hello :: Maybe String -- ^ an optional value for "name" +> -> EitherT ServantError IO HelloMessage +> +> marketing :: ClientInfo -- ^ value for the request body +> -> EitherT ServantError IO Email + +Each function makes available as an argument any value that the response may depend on, as evidenced in the API type. How do we get these functions? Just give a `Proxy` to your API and a host to make the requests to: + +> api :: Proxy API +> api = Proxy +> +> position :<|> hello :<|> marketing = client api (BaseUrl Http "localhost" 8081) + +As you can see in the code above, we just "pattern match our way" to these functions. If we try to derive less or more functions than there are endpoints in the API, we obviously get an error. The `BaseUrl` value there is just: + +``` haskell +-- | URI scheme to use +data Scheme = + Http -- ^ http:// + | Https -- ^ https:// + deriving + +-- | Simple data type to represent the target of HTTP requests +-- for servant's automatically-generated clients. +data BaseUrl = BaseUrl + { baseUrlScheme :: Scheme -- ^ URI scheme to use + , baseUrlHost :: String -- ^ host (eg "haskell.org") + , baseUrlPort :: Int -- ^ port (eg 80) + } +``` + +That's it. Let's now write some code that uses our client functions. + +> queries :: EitherT ServantError IO (Position, HelloMessage, Email) +> queries = do +> pos <- position 10 10 +> msg <- hello (Just "servant") +> em <- marketing (ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]) +> return (pos, msg, em) +> +> run :: IO () +> run = do +> res <- runEitherT queries +> case res of +> Left err -> putStrLn $ "Error: " ++ show err +> Right (pos, msg, em) -> do +> print pos +> print msg +> print em + +You can now run `dist/build/tutorial/tutorial 8` (the server) and +`dist/build/t8-main/t8-main` (the client) to see them both in action. + +``` bash + $ dist/build/tutorial/tutorial 8 + # and in another terminal: + $ dist/build/t8-main/t8-main + Position {x = 10, y = 10} + HelloMessage {msg = "Hello, servant"} + Email {from = "great@company.com", to = "alp@foo.com", subject = "Hey Alp, we miss you!", body = "Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!"} +``` + +The types of the arguments for the functions are the same as for (server-side) request handlers. You now know how to use *servant-client*! + + diff --git a/tutorial/conf.py b/tutorial/conf.py new file mode 100644 index 00000000..6d4f897d --- /dev/null +++ b/tutorial/conf.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- +# +# generics-eot documentation build configuration file, created by +# sphinx-quickstart on Fri Jan 22 12:22:48 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +from recommonmark.parser import CommonMarkParser + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +source_suffix = ['.md', '.rst'] + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'generics-eot' +copyright = u'2016, Sönke Hahn' +author = u'Sönke Hahn' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0.1' +# The full version, including alpha/beta/rc tags. +release = u'0.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build', 'venv'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'generics-eotdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'generics-eot.tex', u'generics-eot Documentation', + u'Sönke Hahn', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'generics-eot', u'generics-eot Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'generics-eot', u'generics-eot Documentation', + author, 'generics-eot', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + +source_parsers = { + '.md': CommonMarkParser, +} diff --git a/tutorial/docs.lhs b/tutorial/docs.lhs new file mode 100644 index 00000000..cb662a54 --- /dev/null +++ b/tutorial/docs.lhs @@ -0,0 +1,227 @@ +--- +title: Generating documentation from API types +toc: true +--- + +The source for this tutorial section is a literate haskell file, so first we +need to have some language extensions and imports: + +> {-# LANGUAGE DataKinds #-} +> {-# LANGUAGE DeriveGeneric #-} +> {-# LANGUAGE FlexibleInstances #-} +> {-# LANGUAGE MultiParamTypeClasses #-} +> {-# LANGUAGE OverloadedStrings #-} +> {-# LANGUAGE TypeOperators #-} +> {-# OPTIONS_GHC -fno-warn-orphans #-} +> +> module Docs where +> +> import Data.ByteString.Lazy (ByteString) +> import Data.Proxy +> import Data.Text.Lazy.Encoding (encodeUtf8) +> import Data.Text.Lazy (pack) +> import Network.HTTP.Types +> import Network.Wai +> import Servant.API +> import Servant.Docs +> import Servant.Server + +And we'll import some things from one of our earlier modules +([Serving an API](/tutorial/server.html)): + +> import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..), +> server3, emailForClient) + +Like client function generation, documentation generation amounts to inspecting the API type and extracting all the data we need to then present it in some format to users of your API. + +This time however, we have to assist *servant*. While it is able to deduce a lot of things about our API, it can't magically come up with descriptions of the various pieces of our APIs that are human-friendly and explain what's going on "at the business-logic level". A good example to study for documentation generation is our webservice with the `/position`, `/hello` and `/marketing` endpoints from earlier: + +> type ExampleAPI = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position +> :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage +> :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email +> +> exampleAPI :: Proxy ExampleAPI +> exampleAPI = Proxy + +While *servant* can see e.g. that there are 3 endpoints and that the response bodies will be in JSON, it doesn't know what influence the captures, parameters, request bodies and other combinators have on the webservice. This is where some manual work is required. + +For every capture, request body, response body, query param, we have to give some explanations about how it influences the response, what values are possible and the likes. Here's how it looks like for the parameters we have above. + +> instance ToCapture (Capture "x" Int) where +> toCapture _ = +> DocCapture "x" -- name +> "(integer) position on the x axis" -- description +> +> instance ToCapture (Capture "y" Int) where +> toCapture _ = +> DocCapture "y" -- name +> "(integer) position on the y axis" -- description +> +> instance ToSample Position Position where +> toSample _ = Just (Position 3 14) -- example of output +> +> instance ToParam (QueryParam "name" String) where +> toParam _ = +> DocQueryParam "name" -- name +> ["Alp", "John Doe", "..."] -- example of values (not necessarily exhaustive) +> "Name of the person to say hello to." -- description +> Normal -- Normal, List or Flag +> +> instance ToSample HelloMessage HelloMessage where +> toSamples _ = +> [ ("When a value is provided for 'name'", HelloMessage "Hello, Alp") +> , ("When 'name' is not specified", HelloMessage "Hello, anonymous coward") +> ] +> -- mutliple examples to display this time +> +> ci :: ClientInfo +> ci = ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"] +> +> instance ToSample ClientInfo ClientInfo where +> toSample _ = Just ci +> +> instance ToSample Email Email where +> toSample _ = Just (emailForClient ci) + +Types that are used as request or response bodies have to instantiate the `ToSample` typeclass which lets you specify one or more examples of values. `Capture`s and `QueryParam`s have to instantiate their respective `ToCapture` and `ToParam` classes and provide a name and some information about the concrete meaning of that argument, as illustrated in the code above. + +With all of this, we can derive docs for our API. + +> apiDocs :: API +> apiDocs = docs exampleAPI + +`API` is a type provided by *servant-docs* that stores all the information one needs about a web API in order to generate documentation in some format. Out of the box, *servant-docs* only provides a pretty documentation printer that outputs [Markdown](http://en.wikipedia.org/wiki/Markdown), but the [servant-pandoc](http://hackage.haskell.org/package/servant-pandoc) package can be used to target many useful formats. + +*servant*'s markdown pretty printer is a function named `markdown`. + +``` haskell +markdown :: API -> String +``` + +That lets us see what our API docs look down in markdown, by looking at `markdown apiDocs`. + +``` text + ## Welcome + + This is our super webservice's API. + + Enjoy! + + ## GET /hello + + #### GET Parameters: + + - name + - **Values**: *Alp, John Doe, ...* + - **Description**: Name of the person to say hello to. + + + #### Response: + + - Status code 200 + - Headers: [] + + - Supported content types are: + + - `application/json` + + - When a value is provided for 'name' + + ```javascript + {"msg":"Hello, Alp"} + ``` + + - When 'name' is not specified + + ```javascript + {"msg":"Hello, anonymous coward"} + ``` + + ## POST /marketing + + #### Request: + + - Supported content types are: + + - `application/json` + + - Example: `application/json` + + ```javascript + {"email":"alp@foo.com","interested_in":["haskell","mathematics"],"age":26,"name":"Alp"} + ``` + + #### Response: + + - Status code 201 + - Headers: [] + + - Supported content types are: + + - `application/json` + + - Response body as below. + + ```javascript + {"subject":"Hey Alp, we miss you!","body":"Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"} + ``` + + ## GET /position/:x/:y + + #### Captures: + + - *x*: (integer) position on the x axis + - *y*: (integer) position on the y axis + + #### Response: + + - Status code 200 + - Headers: [] + + - Supported content types are: + + - `application/json` + + - Response body as below. + + ```javascript + {"x":3,"y":14} + ``` + +``` + +However, we can also add one or more introduction sections to the document. We just need to tweak the way we generate `apiDocs`. We will also convert the content to a lazy `ByteString` since this is what *wai* expects for `Raw` endpoints. + +> docsBS :: ByteString +> docsBS = encodeUtf8 +> . pack +> . markdown +> $ docsWithIntros [intro] exampleAPI +> +> where intro = DocIntro "Welcome" ["This is our super webservice's API.", "Enjoy!"] + +`docsWithIntros` just takes an additional parameter, a list of `DocIntro`s that must be displayed before any endpoint docs. + +We can now serve the API *and* the API docs with a simple server. + +> type DocsAPI = ExampleAPI :<|> Raw +> +> api :: Proxy DocsAPI +> api = Proxy +> +> server :: Server DocsAPI +> server = Server.server3 :<|> serveDocs +> +> where serveDocs _ respond = +> respond $ responseLBS ok200 [plain] docsBS +> +> plain = ("Content-Type", "text/plain") +> +> app :: Application +> app = serve api server + +And if you spin up this server with `dist/build/tutorial/tutorial 10` and go to anywhere else than `/position`, `/hello` and `/marketing`, you will see the API docs in markdown. This is because `serveDocs` is attempted if the 3 other endpoints don't match and systematically succeeds since its definition is to just return some fixed bytestring with the `text/plain` content type. + + diff --git a/tutorial/index.rst b/tutorial/index.rst new file mode 100644 index 00000000..378155e1 --- /dev/null +++ b/tutorial/index.rst @@ -0,0 +1,68 @@ +Servant tutorial +================ + +This is an introductory tutorial to the current version of *servant*, which is **0.4**. Any comment or issue can be directed to [this website's issue tracker](http://github.com/haskell-servant/haskell-servant.github.io/issues). + +Github +------- + +- the servant packages: [haskell-servant/servant](https://github.com/haskell-servant/servant) +- the website (including this tutorial): [haskell-servant/haskell-servant.github.io](https://github.com/haskell-servant/haskell-servant.github.io/) +- Feel free to use the issue tracker (or to send PRs!) on the website's repository to give feedback and suggestions about this tutorial + +Introduction +------------- + +*servant* has the following guiding principles: + +- concision + + This is a pretty wide-ranging principle. You should be able to get nice + documentation for your web servers, and client libraries, without repeating + yourself. You should not have to manually serialize and deserialize your + resources, but only declare how to do those things *once per type*. If a + bunch of your handlers take the same query parameters, you shouldn't have to + repeat that logic for each handler, but instead just "apply" it to all of + them at once. Your handlers shouldn't be where composition goes to die. And + so on. + +- flexibility + + If we haven't thought of your use case, it should still be easily + achievable. If you want to use templating library X, go ahead. Forms? Do + them however you want, but without difficulty. We're not opinionated. + +- separation of concerns + + Your handlers and your HTTP logic should be separate. True to the philosphy + at the core of HTTP and REST, with *servant* your handlers return normal + Haskell datatypes - that's the resource. And then from a description of your + API, *servant* handles the *presentation* (i.e., the Content-Types). But + that's just one example. + +- type safety + + Want to be sure your API meets a specification? Your compiler can check + that for you. Links you can be sure exist? You got it. + +To stick true to these principles, we do things a little differently than you +might expect. The core idea is *reifying the description of your API*. Once +reified, everything follows. We think we might be the first web framework to +reify API descriptions in an extensible way. We're pretty sure we're the first +to reify it as *types*. + +To be able to write a webservice you only need to read the first two sections, +but the goal of this document being to get you started with servant, we also +cover the couple of ways you can extend servant for a great good. + +Tutorial +--------- + +.. toctree:: + :maxdepth: 2 + + api-type.lhs + server.lhs + client.lhs + javascript.lhs + docs.lhs diff --git a/tutorial/javascript.lhs b/tutorial/javascript.lhs new file mode 100644 index 00000000..33b4f73b --- /dev/null +++ b/tutorial/javascript.lhs @@ -0,0 +1,175 @@ +--- +title: Deriving Javascript functions to query an API +toc: true +--- + +We will now see how *servant* lets you turn an API type into javascript +functions that you can call to query a webservice. The derived code assumes you +use *jQuery* but you could very easily adapt the code to generate ajax requests +based on vanilla javascript or another library than *jQuery*. + +For this, we will consider a simple page divided in two parts. At the top, we +will have a search box that lets us search in a list of Haskell books by +author/title with a list of results that gets updated every time we enter or +remove a character, while at the bottom we will be able to see the classical +[probabilistic method to approximate +pi](http://en.wikipedia.org/wiki/Approximations_of_%CF%80#Summing_a_circle.27s_area), +using a webservice to get random points. Finally, we will serve an HTML file +along with a couple of Javascript files, among which one that's automatically +generated from the API type and which will provide ready-to-use functions to +query your API. + +The source for this tutorial section is a literate haskell file, so first we +need to have some language extensions and imports: + +> {-# LANGUAGE DataKinds #-} +> {-# LANGUAGE DeriveGeneric #-} +> {-# LANGUAGE OverloadedStrings #-} +> {-# LANGUAGE TypeOperators #-} +> +> module Javascript where +> +> import Control.Monad.IO.Class +> import Data.Aeson +> import Data.Proxy +> import Data.Text (Text) +> import qualified Data.Text as T +> import GHC.Generics +> import Language.Javascript.JQuery +> import Network.Wai +> import Servant +> import Servant.JQuery +> import System.Random + +Now let's have the API type(s) and the accompanying datatypes. + +> type API = "point" :> Get '[JSON] Point +> :<|> "books" :> QueryParam "q" Text :> Get '[JSON] (Search Book) +> +> type API' = API :<|> Raw +> +> data Point = Point +> { x :: Double +> , y :: Double +> } deriving Generic +> +> instance ToJSON Point +> +> data Search a = Search +> { query :: Text +> , results :: [a] +> } deriving Generic +> +> mkSearch :: Text -> [a] -> Search a +> mkSearch = Search +> +> instance ToJSON a => ToJSON (Search a) +> +> data Book = Book +> { author :: Text +> , title :: Text +> , year :: Int +> } deriving Generic +> +> instance ToJSON Book +> +> book :: Text -> Text -> Int -> Book +> book = Book + +We need a "book database". For the purpose of this guide, let's restrict ourselves to the following books. + +> books :: [Book] +> books = +> [ book "Paul Hudak" "The Haskell School of Expression: Learning Functional Programming through Multimedia" 2000 +> , book "Bryan O'Sullivan, Don Stewart, and John Goerzen" "Real World Haskell" 2008 +> , book "Miran Lipovača" "Learn You a Haskell for Great Good!" 2011 +> , book "Graham Hutton" "Programming in Haskell" 2007 +> , book "Simon Marlow" "Parallel and Concurrent Programming in Haskell" 2013 +> , book "Richard Bird" "Introduction to Functional Programming using Haskell" 1998 +> ] + +Now, given an optional search string `q`, we want to perform a case insensitive search in that list of books. We're obviously not going to try and implement the best possible algorithm, this is out of scope for this tutorial. The following simple linear scan will do, given how small our list is. + +> searchBook :: Monad m => Maybe Text -> m (Search Book) +> searchBook Nothing = return (mkSearch "" books) +> searchBook (Just q) = return (mkSearch q books') +> +> where books' = filter (\b -> q' `T.isInfixOf` T.toLower (author b) +> || q' `T.isInfixOf` T.toLower (title b) +> ) +> books +> q' = T.toLower q + +We also need an endpoint that generates random points `(x, y)` with `-1 <= x,y <= 1`. The code below uses [random](http://hackage.haskell.org/package/random)'s `System.Random`. + +> randomPoint :: MonadIO m => m Point +> randomPoint = liftIO . getStdRandom $ \g -> +> let (rx, g') = randomR (-1, 1) g +> (ry, g'') = randomR (-1, 1) g' +> in (Point rx ry, g'') + +If we add static file serving, our server is now complete. + +> api :: Proxy API +> api = Proxy +> +> api' :: Proxy API' +> api' = Proxy +> +> server :: Server API +> server = randomPoint +> :<|> searchBook +> +> server' :: Server API' +> server' = server +> :<|> serveDirectory "tutorial/t9" +> +> app :: Application +> app = serve api' server' + +Why two different API types, proxies and servers though? Simply because we don't want to generate javascript functions for the `Raw` part of our API type, so we need a `Proxy` for our API type `API'` without its `Raw` endpoint. + +Very similarly to how one can derive haskell functions, we can derive the javascript with just a simple function call to `jsForAPI` from `Servant.JQuery`. + +> apiJS :: String +> apiJS = jsForAPI api + +This `String` contains 2 Javascript functions: + +``` javascript + +function getpoint(onSuccess, onError) +{ + $.ajax( + { url: '/point' + , success: onSuccess + , error: onError + , method: 'GET' + }); +} + +function getbooks(q, onSuccess, onError) +{ + $.ajax( + { url: '/books' + '?q=' + encodeURIComponent(q) + , success: onSuccess + , error: onError + , method: 'GET' + }); +} +``` + +Right before starting up our server, we will need to write this `String` to a file, say `api.js`, along with a copy of the *jQuery* library, as provided by the [js-jquery](http://hackage.haskell.org/package/js-jquery) package. + +> writeJSFiles :: IO () +> writeJSFiles = do +> writeFile "getting-started/gs9/api.js" apiJS +> jq <- readFile =<< Language.Javascript.JQuery.file +> writeFile "getting-started/gs9/jq.js" jq + +And we're good to go. Start the server with `dist/build/tutorial/tutorial 9` and go to `http://localhost:8081/`. Start typing in the name of one of the authors in our database or part of a book title, and check out how long it takes to approximate π using the method mentioned above. + + diff --git a/tutorial/requirements.txt b/tutorial/requirements.txt new file mode 100644 index 00000000..8f89e4b8 --- /dev/null +++ b/tutorial/requirements.txt @@ -0,0 +1,25 @@ +alabaster==0.7.7 +argh==0.26.1 +Babel==2.2.0 +backports-abc==0.4 +backports.ssl-match-hostname==3.5.0.1 +certifi==2015.11.20.1 +CommonMark==0.5.4 +docutils==0.12 +Jinja2==2.8 +livereload==2.4.1 +MarkupSafe==0.23 +pathtools==0.1.2 +Pygments==2.1 +pytz==2015.7 +PyYAML==3.11 +recommonmark==0.4.0 +singledispatch==3.4.0.3 +six==1.10.0 +snowballstemmer==1.2.1 +Sphinx==1.3.4 +sphinx-autobuild==0.5.2 +sphinx-rtd-theme==0.1.9 +tornado==4.3 +watchdog==0.8.3 +wheel==0.26.0 diff --git a/tutorial/server.lhs b/tutorial/server.lhs new file mode 100644 index 00000000..32f098ba --- /dev/null +++ b/tutorial/server.lhs @@ -0,0 +1,1176 @@ +--- +title: Serving an API +toc: true +--- + +Enough chit-chat about type-level combinators and representing an API as a +type. Can we have a webservice already? + +If you want to follow along with the code and run the examples while you read this guide: + +``` bash +cabal get servant-examples +cd servant-examples- +cabal sandbox init +cabal install --dependencies-only +cabal configure && cabal build +``` + +This will produce a `tutorial` executable in the +`dist/build/tutorial` directory that just runs the example corresponding +to the number specified as a command line argument: + +``` bash +$ dist/build/tutorial/tutorial +Usage: tutorial N + where N is the number of the example you want to run. +``` + +A first example +=============== + +Equipped with some basic knowledge about the way we represent API, let's now write our first webservice. + +The source for this tutorial section is a literate haskell file, so first we +need to have some language extensions and imports: + +> {-# LANGUAGE DataKinds #-} +> {-# LANGUAGE DeriveGeneric #-} +> {-# LANGUAGE FlexibleInstances #-} +> {-# LANGUAGE GeneralizedNewtypeDeriving #-} +> {-# LANGUAGE MultiParamTypeClasses #-} +> {-# LANGUAGE OverloadedStrings #-} +> {-# LANGUAGE ScopedTypeVariables #-} +> {-# LANGUAGE TypeOperators #-} +> +> module Server where +> +> import Control.Monad.IO.Class +> import Control.Monad.Reader +> import Control.Monad.Trans.Either +> import Data.Aeson +> import Data.Aeson.Types +> import Data.Attoparsec.ByteString +> import Data.ByteString (ByteString) +> import Data.Int +> import Data.List +> import Data.String.Conversions +> import Data.Time.Calendar +> import GHC.Generics +> import Lucid +> import Network.HTTP.Media ((//), (/:)) +> import Network.Wai +> import Network.Wai.Handler.Warp +> import Servant +> import System.Directory +> import Text.Blaze +> import Text.Blaze.Html.Renderer.Utf8 +> import qualified Data.Aeson.Parser +> import qualified Text.Blaze.Html + +``` haskell +{-# LANGUAGE TypeFamilies #-} +``` + +**Important**: the `Servant` module comes from the *servant-server* package, the one that lets us run webservers that implement a particular API type. It reexports all the types from the *servant* package that let you declare API types as well as everything you need to turn your request handlers into a fully-fledged webserver. This means that in your applications, you can just add *servant-server* as a dependency, import `Servant` and not worry about anything else. + +We will write a server that will serve the following API. + +> type UserAPI1 = "users" :> Get '[JSON] [User] + +Here's what we would like to see when making a GET request to `/users`. + +``` javascript +[ {"name": "Isaac Newton", "age": 372, "email": "isaac@newton.co.uk", "registration_date": "1683-03-01"} +, {"name": "Albert Einstein", "age": 136, "email": "ae@mc2.org", "registration_date": "1905-12-01"} +] +``` + +Now let's define our `User` data type and write some instances for it. + +> data User = User +> { name :: String +> , age :: Int +> , email :: String +> , registration_date :: Day +> } deriving (Eq, Show, Generic) +> +> instance ToJSON User + +Nothing funny going on here. But we now can define our list of two users. + +> users1 :: [User] +> users1 = +> [ User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1) +> , User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1) +> ] + +Let's also write our API type. + +``` haskell +type UserAPI1 = "users" :> Get '[JSON] [User] +``` + +We can now take care of writing the actual webservice that will handle requests +to such an API. This one will be very simple, being reduced to just a single +endpoint. The type of the web application is determined by the API type, +through a *type family* named `Server`. (Type families are just functions that +take types as input and return types.) The `Server` type family will compute +the right type that a bunch of request handlers should have just from the +corresponding API type. + +The first thing to know about the `Server` type family is that behind the +scenes it will drive the routing, letting you focus only on the business +logic. The second thing to know is that for each endpoint, your handlers will +by default run in the `EitherT ServantErr IO` monad. This is overridable very +easily, as explained near the end of this guide. Third thing, the type of the +value returned in that monad must be the same as the second argument of the +HTTP method combinator used for the corresponding endpoint. In our case, it +means we must provide a handler of type `EitherT ServantErr IO [User]`. Well, +we have a monad, let's just `return` our list: + +> server1 :: Server UserAPI1 +> server1 = return users1 + +That's it. Now we can turn `server` into an actual webserver using [wai](http://hackage.haskell.org/package/wai) and [warp](http://hackage.haskell.org/package/warp): + +> userAPI :: Proxy UserAPI1 +> userAPI = Proxy +> +> -- 'serve' comes from servant and hands you a WAI Application, +> -- which you can think of as an "abstract" web application, +> -- not yet a webserver. +> app1 :: Application +> app1 = serve userAPI server1 + +The `userAPI` bit is, alas, boilerplate (we need it to guide type inference). +But that's about as much boilerplate as you get. + +And we're done! Let's run our webservice on the port 8081. + +> main :: IO () +> main = run 8081 app1 + +You can put this all into a file or just grab [servant's +repo](http://github.com/haskell-servant/servant) and look at the +*servant-examples* directory. The code we have just explored is in +*tutorial/T1.hs*, runnable with +`dist/build/tutorial/tutorial 1`. + +If you run it, you can go to `http://localhost:8081/users` in your browser or +query it with curl and you see: + +``` bash +$ curl http://localhost:8081/users +[{"email":"isaac@newton.co.uk","registration_date":"1683-03-01","age":372,"name":"Isaac Newton"},{"email":"ae@mc2.org","registration_date":"1905-12-01","age":136,"name":"Albert Einstein"}] +``` + +More endpoints +============== + +What if we want more than one endpoint? Let's add `/albert` and `/isaac` to view the corresponding users encoded in JSON. + +> type UserAPI2 = "users" :> Get '[JSON] [User] +> :<|> "albert" :> Get '[JSON] User +> :<|> "isaac" :> Get '[JSON] User + +And let's adapt our code a bit. + +> isaac :: User +> isaac = User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1) +> +> albert :: User +> albert = User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1) +> +> users2 :: [User] +> users2 = [isaac, albert] + +Now, just like we separate the various endpoints in `UserAPI` with `:<|>`, we +are going to separate the handlers with `:<|>` too! They must be provided in +the same order as the one they appear in in the API type. + +> server2 :: Server UserAPI2 +> server2 = return users2 +> :<|> return albert +> :<|> return isaac + +And that's it! You can run this example with +`dist/build/tutorial/tutorial 2` and check out the data available +at `/users`, `/albert` and `/isaac`. + +From combinators to handler arguments +===================================== + +Fine, we can write trivial webservices easily, but none of the two above use +any "fancy" combinator from servant. Let's address this and use `QueryParam`, +`Capture` and `ReqBody` right away. You'll see how each occurence of these +combinators in an endpoint makes the corresponding handler receive an +argument of the appropriate type automatically. You don't have to worry about +manually looking up URL captures or query string parameters, or +decoding/encoding data from/to JSON. Never. + +We are going to use the following data types and functions to implement a server for `API`. + +> type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position +> :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage +> :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email +> +> data Position = Position +> { x :: Int +> , y :: Int +> } deriving Generic +> +> instance ToJSON Position +> +> newtype HelloMessage = HelloMessage { msg :: String } +> deriving Generic +> +> instance ToJSON HelloMessage +> +> data ClientInfo = ClientInfo +> { clientName :: String +> , clientEmail :: String +> , clientAge :: Int +> , clientInterestedIn :: [String] +> } deriving Generic +> +> instance FromJSON ClientInfo +> instance ToJSON ClientInfo +> +> data Email = Email +> { from :: String +> , to :: String +> , subject :: String +> , body :: String +> } deriving Generic +> +> instance ToJSON Email +> +> emailForClient :: ClientInfo -> Email +> emailForClient c = Email from' to' subject' body' +> +> where from' = "great@company.com" +> to' = clientEmail c +> subject' = "Hey " ++ clientName c ++ ", we miss you!" +> body' = "Hi " ++ clientName c ++ ",\n\n" +> ++ "Since you've recently turned " ++ show (clientAge c) +> ++ ", have you checked out our latest " +> ++ intercalate ", " (clientInterestedIn c) +> ++ " products? Give us a visit!" + +We can implement handlers for the three endpoints: + +> server3 :: Server API +> server3 = position +> :<|> hello +> :<|> marketing +> +> where position :: Int -> Int -> EitherT ServantErr IO Position +> position x y = return (Position x y) +> +> hello :: Maybe String -> EitherT ServantErr IO HelloMessage +> hello mname = return . HelloMessage $ case mname of +> Nothing -> "Hello, anonymous coward" +> Just n -> "Hello, " ++ n +> +> marketing :: ClientInfo -> EitherT ServantErr IO Email +> marketing clientinfo = return (emailForClient clientinfo) + +Did you see that? The types for your handlers changed to be just what we +needed! In particular: + + - a `Capture "something" a` becomes an argument of type `a` (for `position`); + - a `QueryParam "something" a` becomes an argument of type `Maybe a` (because +an endpoint can technically be accessed without specifying any query +string parameter, we decided to "force" handlers to be aware that the +parameter might not always be there); + + - a `ReqBody contentTypeList a` becomes an argument of type `a`; + +And that's it. You can see this example in action by running `dist/build/tutorial/tutorial 3`. + +``` bash +$ curl http://localhost:8081/position/1/2 +{"x":1,"y":2} +$ curl http://localhost:8081/hello +{"msg":"Hello, anonymous coward"} +$ curl http://localhost:8081/hello?name=Alp +{"msg":"Hello, Alp"} +$ curl -X POST -d '{"name":"Alp Mestanogullari", "email" : "alp@foo.com", "age": 25, "interested_in": ["haskell", "mathematics"]}' -H 'Accept: application/json' -H 'Content-type: application/json' http://localhost:8081/marketing +{"subject":"Hey Alp Mestanogullari, we miss you!","body":"Hi Alp Mestanogullari,\n\nSince you've recently turned 25, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"} +``` + +For reference, here's a list of some combinators from *servant* and for those +that get turned into arguments to the handlers, the type of the argument. + + > - `Delete`, `Get`, `Patch`, `Post`, `Put`: these do not become arguments. They provide the return type of handlers, which usually is `EitherT ServantErr IO `. + > - `Capture "something" a` becomes an argument of type `a`. + > - `QueryParam "something" a`, `MatrixParam "something" a`, `Header "something" a` all become arguments of type `Maybe a`, because there might be no value at all specified by the client for these. + > - `QueryFlag "something"` and `MatrixFlag "something"` get turned into arguments of type `Bool`. + > - `QueryParams "something" a` and `MatrixParams "something" a` get turned into arguments of type `[a]`. + > - `ReqBody contentTypes a` gets turned into an argument of type `a`. + +The `FromText`/`ToText` classes +=============================== + +Wait... How does *servant* know how to decode the `Int`s from the URL? Or how +to decode a `ClientInfo` value from the request body? This is what this and the +following two sections address. + +`Capture`s and `QueryParam`s are represented by some textual value in URLs. +`Header`s are similarly represented by a pair of a header name and a +corresponding (textual) value in the request's "metadata". This is why we +decided to provide a pair of typeclasses, `FromText` and `ToText` which just +let you say that you can respectively *extract* or *encode* values of some type +*from*/*to* text. Here are the definitions: + +``` haskell +class FromText a where + fromText :: Text -> Maybe a + +class ToText a where + toText :: a -> Text +``` + +And as long as the type that a `Capture`/`QueryParam`/`Header`/etc will be +decoded to provides a `FromText` instance, it will Just Work. *servant* +provides a decent number of instances, but here are some examples of defining +your own. + +> -- A typical enumeration +> data Direction +> = Up +> | Down +> | Left +> | Right +> +> instance FromText Direction where +> -- requires {-# LANGUAGE OverloadedStrings #-} +> fromText "up" = Just Up +> fromText "down" = Just Down +> fromText "left" = Just Server.Left +> fromText "right" = Just Server.Right +> fromText _ = Nothing +> +> instance ToText Direction where +> toText Up = "up" +> toText Down = "down" +> toText Server.Left = "left" +> toText Server.Right = "right" +> +> newtype UserId = UserId Int64 +> deriving (FromText, ToText) + +or writing the instances by hand: + +``` haskell +instance FromText UserId where + fromText = fmap UserId fromText + +instance ToText UserId where + toText (UserId i) = toText i +``` + +There's not much else to say about these classes. You will need instances for +them when using `Capture`, `QueryParam`, `QueryParams`, `MatrixParam`, +`MatrixParams` and `Header` with your types. You will need `FromText` instances +for server-side request handlers and `ToText` instances only when using +*servant-client*, as described in the [section about deriving haskell +functions to query an API](/tutorial/client.html). + +Using content-types with your data types +======================================== + +The same principle was operating when decoding request bodies from JSON, and +responses *into* JSON. (JSON is just the running example - you can do this with +any content-type.) + +This section introduces a couple of typeclasses provided by *servant* that make +all of this work. + +The truth behind `JSON` +----------------------- + +What exactly is `JSON`? Like the 3 other content types provided out of the box +by *servant*, it's a really dumb data type. + +``` haskell +data JSON +data PlainText +data FormUrlEncoded +data OctetStream +``` + +Obviously, this is not all there is to `JSON`, otherwise it would be quite +pointless. Like most of the data types in *servant*, `JSON` is mostly there as +a special *symbol* that's associated with encoding (resp. decoding) to (resp. +from) the *JSON* format. The way this association is performed can be +decomposed into two steps. + +The first step is to provide a proper +[`MediaType`](https://hackage.haskell.org/package/http-media-0.6.2/docs/Network-HTTP-Media.html) +representation for `JSON`, or for your own content types. If you look at the +haddocks from this link, you can see that we just have to specify +`application/json` using the appropriate functions. In our case, we can just +use `(//) :: ByteString -> ByteString -> MediaType`. The precise way to specify +the `MediaType` is to write an instance for the `Accept` class: + +``` haskell +-- for reference: +class Accept ctype where + contentType :: Proxy ctype -> MediaType + +instance Accept JSON where + contentType _ = "application" // "json" +``` + +The second step is centered around the `MimeRender` and `MimeUnrender` classes. +These classes just let you specify a way to respectively encode and decode +values respectively into or from your content-type's representation. + +``` haskell +class Accept ctype => MimeRender ctype a where + mimeRender :: Proxy ctype -> a -> ByteString + -- alternatively readable as: + mimeRender :: Proxy ctype -> (a -> ByteString) +``` + +Given a content-type and some user type, `MimeRender` provides a function that +encodes values of type `a` to lazy `ByteString`s. + +In the case of `JSON`, this is easily dealt with! For any type `a` with a +`ToJSON` instance, we can render values of that type to JSON using +`Data.Aeson.encode`. + +``` haskell +instance ToJSON a => MimeRender JSON a where + mimeRender _ = encode +``` + +And now the `MimeUnrender` class, which lets us extract values from lazy +`ByteString`s, alternatively failing with an error string. + +``` haskell +class Accept ctype => MimeUnrender ctype a where + mimeUnrender :: Proxy ctype -> ByteString -> Either String a + -- alternatively: + mimeUnrender :: Proxy ctype -> (ByteString -> Either String a) +``` + +We don't have much work to do there either, `Data.Aeson.eitherDecode` is +precisely what we need. However, it only allows arrays and objects as toplevel +JSON values and this has proven to get in our way more than help us so we wrote +our own little function around *aeson* and *attoparsec* that allows any type of +JSON value at the toplevel of a "JSON document". Here's the definition in case +you are curious. + +> eitherDecodeLenient :: FromJSON a => ByteString -> Either String a +> eitherDecodeLenient input = do +> v :: Value <- parseOnly (Data.Aeson.Parser.value <* endOfInput) (cs input) +> parseEither parseJSON v + +This function is exactly what we need for our `MimeUnrender` instance. + +``` haskell +instance FromJSON a => MimeUnrender JSON a where + mimeUnrender _ = eitherDecodeLenient +``` + +And this is all the code that lets you use `JSON` for with `ReqBody`, `Get`, +`Post` and friends. We can check our understanding by implementing support +for an `HTML` content type, so that users of your webservice can access an +HTML representation of the data they want, ready to be included in any HTML +document, e.g. using [jQuery's `load` function](https://api.jquery.com/load/), simply by adding `Accept: +text/html` to their request headers. + +Case-studies: *servant-blaze* and *servant-lucid* +------------------------------------------------- + +These days, most of the haskellers who write their HTML UIs directly from +Haskell use either [blaze-html](http://hackage.haskell.org/package/blaze-html) +or [lucid](http://hackage.haskell.org/package/lucid). The best option for +*servant* is obviously to support both (and hopefully other templating +solutions!). + +> data HTMLLucid + +Once again, the data type is just there as a symbol for the encoding/decoding +functions, except that this time we will only worry about encoding since +*blaze-html* and *lucid* don't provide a way to extract data from HTML. + +Both packages also have the same `Accept` instance for their `HTMLLucid` type. + +> instance Accept HTMLLucid where +> contentType _ = "text" // "html" /: ("charset", "utf-8") + +Note that this instance uses the `(/:)` operator from *http-media* which lets +us specify additional information about a content-type, like the charset here. + +The rendering instances for both packages both call similar functions that take +types with an appropriate instance to an "abstract" HTML representation and +then write that to a `ByteString`. + +For *lucid*: + +> instance ToHtml a => MimeRender HTMLLucid a where +> mimeRender _ = renderBS . toHtml +> +> -- let's also provide an instance for lucid's +> -- 'Html' wrapper. +> instance MimeRender HTMLLucid (Html a) where +> mimeRender _ = renderBS + +For *blaze-html*: + +> -- For this tutorial to compile 'HTMLLucid' and 'HTMLBlaze' have to be +> -- distinct. Usually you would stick to one html rendering library and then +> -- you can go with one 'HTML' type. +> data HTMLBlaze +> +> instance Accept HTMLBlaze where +> contentType _ = "text" // "html" /: ("charset", "utf-8") +> +> instance ToMarkup a => MimeRender HTMLBlaze a where +> mimeRender _ = renderHtml . Text.Blaze.Html.toHtml +> +> -- while we're at it, just like for lucid we can +> -- provide an instance for rendering blaze's 'Html' type +> instance MimeRender HTMLBlaze Text.Blaze.Html.Html where +> mimeRender _ = renderHtml + +Both [servant-blaze](http://hackage.haskell.org/package/servant-blaze) and +[servant-lucid](http://hackage.haskell.org/package/servant-lucid) let you use +`HTMLLucid` in any content type list as long as you provide an instance of the +appropriate class (`ToMarkup` for *blaze-html*, `ToHtml` for *lucid*). + +We can now write webservice that uses *servant-lucid* to show the `HTMLLucid` +content type in action. First off, imports and pragmas as usual. + +We will be serving the following API: + +> type PersonAPI = "persons" :> Get '[JSON, HTMLLucid] [Person] + +where `Person` is defined as follows: + +> data Person = Person +> { firstName :: String +> , lastName :: String +> } deriving Generic -- for the JSON instance +> +> instance ToJSON Person + +Now, let's teach *lucid* how to render a `Person` as a row in a table, and then +a list of `Person`s as a table with a row per person. + +> -- HTML serialization of a single person +> instance ToHtml Person where +> toHtml person = +> tr_ $ do +> td_ (toHtml $ firstName person) +> td_ (toHtml $ lastName person) +> +> -- do not worry too much about this +> toHtmlRaw = toHtml +> +> -- HTML serialization of a list of persons +> instance ToHtml [Person] where +> toHtml persons = table_ $ do +> tr_ $ do +> th_ "first name" +> th_ "last name" +> +> -- this just calls toHtml on each person of the list +> -- and concatenates the resulting pieces of HTML together +> foldMap toHtml persons +> +> toHtmlRaw = toHtml + +We create some `Person` values and serve them as a list: + +> persons :: [Person] +> persons = +> [ Person "Isaac" "Newton" +> , Person "Albert" "Einstein" +> ] +> +> personAPI :: Proxy PersonAPI +> personAPI = Proxy +> +> server4 :: Server PersonAPI +> server4 = return persons +> +> app2 :: Application +> app2 = serve personAPI server4 + +And we're good to go. You can run this example with `dist/build/tutorial/tutorial 4`. + +``` bash + $ curl http://localhost:8081/persons + [{"lastName":"Newton","firstName":"Isaac"},{"lastName":"Einstein","firstName":"Albert"}] + $ curl -H 'Accept: text/html' http://localhost:8081/persons +
first namelast name
IsaacNewton
AlbertEinstein
+ # or just point your browser to http://localhost:8081/persons +``` + +The `EitherT ServantErr IO` monad +================================= + +At the heart of the handlers is the monad they run in, namely `EitherT +ServantErr IO`. One might wonder: why this monad? The answer is that it is the +simplest monad with the following properties: + +- it lets us both return a successful result (with the `Right` branch of +`Either`) or "fail" with a descriptive error (with the `Left` branch of +`Either`); +- it lets us perform IO, which is absolutely vital since most webservices exist +as interfaces to databases that we interact with in `IO`; + +Let's recall some definitions. + +``` haskell +-- from the Prelude +data Either e a = Left e | Right a + +-- from the 'either' package at +-- http://hackage.haskell.org/package/either-4.3.3.2/docs/Control-Monad-Trans-Either.html +newtype EitherT e m a + = EitherT { runEitherT :: m (Either e a) } +``` + +In short, this means that a handler of type `EitherT ServantErr IO a` is simply +equivalent to a computation of type `IO (Either ServantErr a)`, that is, an IO +action that either returns an error or a result. + +The aforementioned `either` package is worth taking a look at. Perhaps most +importantly: + +``` haskell +left :: Monad m => e -> EitherT e m a +``` +Allows you to return an error from your handler (whereas `return` is enough to +return a success). + +Most of what you'll be doing in your handlers is running some IO and, +depending on the result, you might sometimes want to throw an error of some +kind and abort early. The next two sections cover how to do just that. + +Performing IO +------------- + +Another important instance from the list above is `MonadIO m => MonadIO (EitherT e m)`. [`MonadIO`](http://hackage.haskell.org/package/transformers-0.4.3.0/docs/Control-Monad-IO-Class.html) is a class from the *transformers* package defined as: + +``` haskell +class Monad m => MonadIO m where + liftIO :: IO a -> m a +``` + +Obviously, the `IO` monad provides a `MonadIO` instance. Hence for any type `e`, `EitherT e IO` has a `MonadIO` instance. So if you want to run any kind of IO computation in your handlers, just use `liftIO`: + +> type IOAPI1 = "myfile.txt" :> Get '[JSON] FileContent +> +> newtype FileContent = FileContent +> { content :: String } +> deriving Generic +> +> instance ToJSON FileContent +> +> server5 :: Server IOAPI1 +> server5 = do +> filecontent <- liftIO (readFile "myfile.txt") +> return (FileContent filecontent) + +Failing, through `ServantErr` +----------------------------- + +If you want to explicitly fail at providing the result promised by an endpoint +using the appropriate HTTP status code (not found, unauthorized, etc) and some +error message, all you have to do is use the `left` function mentioned above +and provide it with the appropriate value of type `ServantErr`, which is +defined as: + +``` haskell +data ServantErr = ServantErr + { errHTTPCode :: Int + , errReasonPhrase :: String + , errBody :: ByteString -- lazy bytestring + , errHeaders :: [Header] + } +``` + +Many standard values are provided out of the box by the `Servant.Server` +module. If you want to use these values but add a body or some headers, just +use record update syntax: + +> failingHandler :: EitherT ServantErr IO () +> failingHandler = left myerr +> +> where myerr :: ServantErr +> myerr = err503 { errBody = "Sorry dear user." } + +Here's an example where we return a customised 404-Not-Found error message in +the response body if "myfile.txt" isn't there: + +> server6 :: Server IOAPI1 +> server6 = do +> exists <- liftIO (doesFileExist "myfile.txt") +> if exists +> then liftIO (readFile "myfile.txt") >>= return . FileContent +> else left custom404Err +> +> where custom404Err = err404 { errBody = "myfile.txt just isn't there, please leave this server alone." } + +Let's run this server (`dist/build/tutorial/tutorial 5`) and +query it, first without the file and then with the file. + +``` bash + $ curl --verbose http://localhost:8081/myfile.txt + [snip] + * Connected to localhost (127.0.0.1) port 8081 (#0) + > GET /myfile.txt HTTP/1.1 + > User-Agent: curl/7.30.0 + > Host: localhost:8081 + > Accept: */* + > + < HTTP/1.1 404 Not Found + [snip] + myfile.txt just isnt there, please leave this server alone. + + $ echo Hello > myfile.txt + + $ curl --verbose http://localhost:8081/myfile.txt + [snip] + * Connected to localhost (127.0.0.1) port 8081 (#0) + > GET /myfile.txt HTTP/1.1 + > User-Agent: curl/7.30.0 + > Host: localhost:8081 + > Accept: */* + > + < HTTP/1.1 200 OK + [snip] + < Content-Type: application/json + [snip] + {"content":"Hello\n"} +``` + +Response headers +================ + +To add headers to your response, use [addHeader](http://hackage.haskell.org/package/servant-0.4.4/docs/Servant-API-ResponseHeaders.html). +Note that this changes the type of your API, as we can see in the following example: + +> type MyHandler = Get '[JSON] (Headers '[Header "X-An-Int" Int] User) +> +> myHandler :: Server MyHandler +> myHandler = return $ addHeader 1797 albert + + +Serving static files +==================== + +*servant-server* also provides a way to just serve the content of a directory +under some path in your web API. As mentioned earlier in this document, the +`Raw` combinator can be used in your APIs to mean "plug here any WAI +application". Well, servant-server provides a function to get a file and +directory serving WAI application, namely: + +``` haskell +-- exported by Servant and Servant.Server +serveDirectory :: FilePath -> Server Raw +``` + +`serveDirectory`'s argument must be a path to a valid directory. You can see an +example below, runnable with `dist/build/tutorial/tutorial 6` +(you **must** run it from within the *servant-examples/* directory!), which is +a webserver that serves the various bits of code covered in this +getting-started. + +The API type will be the following. + +> type CodeAPI = "code" :> Raw + +And the server: + +> codeAPI :: Proxy CodeAPI +> codeAPI = Proxy + +> server7 :: Server CodeAPI +> server7 = serveDirectory "tutorial" +> +> app3 :: Application +> app3 = serve codeAPI server7 + +This server will match any request whose path starts with `/code` and will look for a file at the path described by the rest of the request path, inside the *tutorial/* directory of the path you run the program from. + +In other words: + +- If a client requests `/code/foo.txt`, the server will look for a file at `./tutorial/foo.txt` (and fail) +- If a client requests `/code/T1.hs`, the server will look for a file at `./tutorial/T1.hs` (and succeed) +- If a client requests `/code/foo/bar/baz/movie.mp4`, the server will look for a file at `./tutorial/foo/bar/baz/movie.mp4` (and fail) + +Here is our little server in action. + +``` haskell +$ curl http://localhost:8081/code/T1.hs +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE TypeOperators #-} +module T1 where + +import Data.Aeson +import Data.Time.Calendar +import GHC.Generics +import Network.Wai +import Servant + +data User = User + { name :: String + , age :: Int + , email :: String + , registration_date :: Day + } deriving (Eq, Show, Generic) + +-- orphan ToJSON instance for Day. necessary to derive one for User +instance ToJSON Day where + -- display a day in YYYY-mm-dd format + toJSON d = toJSON (showGregorian d) + +instance ToJSON User + +type UserAPI = "users" :> Get '[JSON] [User] + +users :: [User] +users = + [ User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1) + , User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1) + ] + +userAPI :: Proxy UserAPI +userAPI = Proxy + +server :: Server UserAPI +server = return users + +app :: Application +app = serve userAPI server +$ curl http://localhost:8081/code/tutorial.hs +import Network.Wai +import Network.Wai.Handler.Warp +import System.Environment + +import qualified T1 +import qualified T2 +import qualified T3 +import qualified T4 +import qualified T5 +import qualified T6 +import qualified T7 +import qualified T9 +import qualified T10 + +app :: String -> (Application -> IO ()) -> IO () +app n f = case n of + "1" -> f T1.app + "2" -> f T2.app + "3" -> f T3.app + "4" -> f T4.app + "5" -> f T5.app + "6" -> f T6.app + "7" -> f T7.app + "8" -> f T3.app + "9" -> T9.writeJSFiles >> f T9.app + "10" -> f T10.app + _ -> usage + +main :: IO () +main = do + args <- getArgs + case args of + [n] -> app n (run 8081) + _ -> usage + +usage :: IO () +usage = do + putStrLn "Usage:\t tutorial N" + putStrLn "\t\twhere N is the number of the example you want to run." + +$ curl http://localhost:8081/foo +not found +``` + +Nested APIs +=========== + +Let's see how you can define APIs in a modular way, while avoiding repetition. Consider this simple example: + +> type UserAPI3 = -- view the user with given userid, in JSON +> Capture "userid" Int :> Get '[JSON] User +> +> :<|> -- delete the user with given userid. empty response +> Capture "userid" Int :> Delete '[] () + +We can instead factor out the `userid`: + +> type UserAPI4 = Capture "userid" Int :> +> ( Get '[JSON] User +> :<|> Delete '[] () +> ) + +However, you have to be aware that this has an effect on the type of the corresponding `Server`: + +``` haskell +Server UserAPI3 = (Int -> EitherT ServantErr IO User) + :<|> (Int -> EitherT ServantErr IO ()) + +Server UserAPI4 = Int -> ( EitherT ServantErr IO User + :<|> EitherT ServantErr IO () + ) +``` + +In the first case, each handler receives the *userid* argument. In the latter, +the whole `Server` takes the *userid* and has handlers that are just computations in `EitherT`, with no arguments. In other words: + +> server8 :: Server UserAPI3 +> server8 = getUser :<|> deleteUser +> +> where getUser :: Int -> EitherT ServantErr IO User +> getUser _userid = error "..." +> +> deleteUser :: Int -> EitherT ServantErr IO () +> deleteUser _userid = error "..." +> +> -- notice how getUser and deleteUser +> -- have a different type! no argument anymore, +> -- the argument directly goes to the whole Server +> server9 :: Server UserAPI4 +> server9 userid = getUser userid :<|> deleteUser userid +> +> where getUser :: Int -> EitherT ServantErr IO User +> getUser = error "..." +> +> deleteUser :: Int -> EitherT ServantErr IO () +> deleteUser = error "..." + +Note that there's nothing special about `Capture` that lets you "factor it out": this can be done with any combinator. Here are a few examples of APIs with a combinator factored out for which we can write a perfectly valid `Server`. + +> -- we just factor out the "users" path fragment +> type API1 = "users" :> +> ( Get '[JSON] [User] -- user listing +> :<|> Capture "userid" Int :> Get '[JSON] User -- view a particular user +> ) +> +> -- we factor out the Request Body +> type API2 = ReqBody '[JSON] User :> +> ( Get '[JSON] User -- just display the same user back, don't register it +> :<|> Post '[JSON] () -- register the user. empty response +> ) +> +> -- we factor out a Header +> type API3 = Header "Authorization" Token :> +> ( Get '[JSON] SecretData -- get some secret data, if authorized +> :<|> ReqBody '[JSON] SecretData :> Post '[] () -- add some secret data, if authorized +> ) +> +> newtype Token = Token ByteString +> newtype SecretData = SecretData ByteString + +This approach lets you define APIs modularly and assemble them all into one big API type only at the end. + +> type UsersAPI = +> Get '[JSON] [User] -- list users +> :<|> ReqBody '[JSON] User :> Post '[] () -- add a user +> :<|> Capture "userid" Int :> +> ( Get '[JSON] User -- view a user +> :<|> ReqBody '[JSON] User :> Put '[] () -- update a user +> :<|> Delete '[] () -- delete a user +> ) +> +> usersServer :: Server UsersAPI +> usersServer = getUsers :<|> newUser :<|> userOperations +> +> where getUsers :: EitherT ServantErr IO [User] +> getUsers = error "..." +> +> newUser :: User -> EitherT ServantErr IO () +> newUser = error "..." +> +> userOperations userid = +> viewUser userid :<|> updateUser userid :<|> deleteUser userid +> +> where +> viewUser :: Int -> EitherT ServantErr IO User +> viewUser = error "..." +> +> updateUser :: Int -> User -> EitherT ServantErr IO () +> updateUser = error "..." +> +> deleteUser :: Int -> EitherT ServantErr IO () +> deleteUser = error "..." + +> type ProductsAPI = +> Get '[JSON] [Product] -- list products +> :<|> ReqBody '[JSON] Product :> Post '[] () -- add a product +> :<|> Capture "productid" Int :> +> ( Get '[JSON] Product -- view a product +> :<|> ReqBody '[JSON] Product :> Put '[] () -- update a product +> :<|> Delete '[] () -- delete a product +> ) +> +> data Product = Product { productId :: Int } +> +> productsServer :: Server ProductsAPI +> productsServer = getProducts :<|> newProduct :<|> productOperations +> +> where getProducts :: EitherT ServantErr IO [Product] +> getProducts = error "..." +> +> newProduct :: Product -> EitherT ServantErr IO () +> newProduct = error "..." +> +> productOperations productid = +> viewProduct productid :<|> updateProduct productid :<|> deleteProduct productid +> +> where +> viewProduct :: Int -> EitherT ServantErr IO Product +> viewProduct = error "..." +> +> updateProduct :: Int -> Product -> EitherT ServantErr IO () +> updateProduct = error "..." +> +> deleteProduct :: Int -> EitherT ServantErr IO () +> deleteProduct = error "..." + +> type CombinedAPI = "users" :> UsersAPI +> :<|> "products" :> ProductsAPI +> +> server10 :: Server CombinedAPI +> server10 = usersServer :<|> productsServer + +Finally, we can realize the user and product APIs are quite similar and abstract that away: + +> -- API for values of type 'a' +> -- indexed by values of type 'i' +> type APIFor a i = +> Get '[JSON] [a] -- list 'a's +> :<|> ReqBody '[JSON] a :> Post '[] () -- add an 'a' +> :<|> Capture "id" i :> +> ( Get '[JSON] a -- view an 'a' given its "identifier" of type 'i' +> :<|> ReqBody '[JSON] a :> Put '[] () -- update an 'a' +> :<|> Delete '[] () -- delete an 'a' +> ) +> +> -- Build the appropriate 'Server' +> -- given the handlers of the right type. +> serverFor :: EitherT ServantErr IO [a] -- handler for listing of 'a's +> -> (a -> EitherT ServantErr IO ()) -- handler for adding an 'a' +> -> (i -> EitherT ServantErr IO a) -- handler for viewing an 'a' given its identifier of type 'i' +> -> (i -> a -> EitherT ServantErr IO ()) -- updating an 'a' with given id +> -> (i -> EitherT ServantErr IO ()) -- deleting an 'a' given its id +> -> Server (APIFor a i) +> serverFor = error "..." +> -- implementation left as an exercise. contact us on IRC +> -- or the mailing list if you get stuck! + +Using another monad for your handlers +===================================== + +Remember how `Server` turns combinators for HTTP methods into `EitherT ServantErr IO`? Well, actually, there's more to that. `Server` is actually a simple type synonym. + +``` haskell +type Server api = ServerT api (EitherT ServantErr IO) +``` + +`ServerT` is the actual type family that computes the required types for the handlers that's part of the `HasServer` class. It's like `Server` except that it takes a third parameter which is the monad you want your handlers to run in, or more generally the return types of your handlers. This third parameter is used for specifying the return type of the handler for an endpoint, e.g when computing `ServerT (Get '[JSON] Person) SomeMonad`. The result would be `SomeMonad Person`. + +The first and main question one might have then is: how do we write handlers that run in another monad? How can we "bring back" the value from a given monad into something *servant* can understand? + +Natural transformations +----------------------- + +If we have a function that gets us from an `m a` to an `n a`, for any `a`, what +do we have? + +``` haskell +newtype m :~> n = Nat { unNat :: forall a. m a -> n a} + +-- For example +-- listToMaybeNat ::`[] :~> Maybe` +-- listToMaybeNat = Nat listToMaybe -- from Data.Maybe +``` +(`Nat` comes from "natural transformation", in case you're wondering.) + +So if you want to write handlers using another monad/type than `EitherT +ServantErr IO`, say the `Reader String` monad, the first thing you have to +prepare is a function: + +``` haskell +readerToEither :: Reader String :~> EitherT ServantErr IO +``` + +Let's start with `readerToEither'`. We obviously have to run the `Reader` +computation by supplying it with a `String`, like `"hi"`. We get an `a` out +from that and can then just `return` it into `EitherT`. We can then just wrap +that function with the `Nat` constructor to make it have the fancier type. + +> readerToEither' :: forall a. Reader String a -> EitherT ServantErr IO a +> readerToEither' r = return (runReader r "hi") +> +> readerToEither :: Reader String :~> EitherT ServantErr IO +> readerToEither = Nat readerToEither' + +We can write some simple webservice with the handlers running in `Reader String`. + +> type ReaderAPI = "a" :> Get '[JSON] Int +> :<|> "b" :> Get '[JSON] String +> +> readerAPI :: Proxy ReaderAPI +> readerAPI = Proxy +> +> readerServerT :: ServerT ReaderAPI (Reader String) +> readerServerT = a :<|> b +> +> where a :: Reader String Int +> a = return 1797 +> +> b :: Reader String String +> b = ask + +We unfortunately can't use `readerServerT` as an argument of `serve`, because +`serve` wants a `Server ReaderAPI`, i.e., with handlers running in `EitherT +ServantErr IO`. But there's a simple solution to this. + +Enter `enter` +------------- + +That's right. We have just written `readerToEither`, which is exactly what we +would need to apply to the results of all handlers to make the handlers have the +right type for `serve`. Being cumbersome to do by hand, we provide a function +`enter` which takes a natural transformation between two parametrized types `m` +and `n` and a `ServerT someapi m`, and returns a `ServerT someapi n`. + +In our case, we can wrap up our little webservice by using `enter readerToEither` on our handlers. + +> readerServer :: Server ReaderAPI +> readerServer = enter readerToEither readerServerT +> +> app4 :: Application +> app4 = serve readerAPI readerServer + +And we can indeed see this webservice in action by running `dist/build/tutorial/tutorial 7`. + +``` bash +$ curl http://localhost:8081/a +1797 +$ curl http://localhost:8081/b +"hi" +``` + +Conclusion +========== + +You're now equipped to write any kind of webservice/web-application using *servant*. One thing not covered here is how to incorporate your own combinators and will be the topic of a page on the website. The rest of this document focuses on *servant-client*, *servant-jquery* and *servant-docs*. + + From 6c0a7ba8d85b29a3a43342906337188bedd99d7b Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Mon, 25 Jan 2016 14:24:38 +0100 Subject: [PATCH 03/50] Fix links --- tutorial/conf.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tutorial/conf.py b/tutorial/conf.py index 6d4f897d..832c0cf8 100644 --- a/tutorial/conf.py +++ b/tutorial/conf.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# generics-eot documentation build configuration file, created by +# servant documentation build configuration file, created by # sphinx-quickstart on Fri Jan 22 12:22:48 2016. # # This file is execfile()d with the current directory set to its @@ -36,7 +36,7 @@ templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: -source_suffix = ['.md', '.rst'] +source_suffix = ['.md', '.rst', '.lhs'] # The encoding of source files. #source_encoding = 'utf-8-sig' @@ -45,9 +45,9 @@ source_suffix = ['.md', '.rst'] master_doc = 'index' # General information about the project. -project = u'generics-eot' -copyright = u'2016, Sönke Hahn' -author = u'Sönke Hahn' +project = u'servant' +copyright = u'2016, Servant Contributors' +author = u'Servant Contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -199,7 +199,7 @@ html_static_path = ['_static'] #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'generics-eotdoc' +htmlhelp_basename = 'servantdoc' # -- Options for LaTeX output --------------------------------------------- @@ -221,8 +221,8 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'generics-eot.tex', u'generics-eot Documentation', - u'Sönke Hahn', 'manual'), + (master_doc, 'servant.tex', u'servant Documentation', + u'Servant Contributors', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -251,7 +251,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'generics-eot', u'generics-eot Documentation', + (master_doc, 'servant', u'servant Documentation', [author], 1) ] @@ -265,8 +265,8 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'generics-eot', u'generics-eot Documentation', - author, 'generics-eot', 'One line description of project.', + (master_doc, 'servant', u'servant Documentation', + author, 'servant', 'One line description of project.', 'Miscellaneous'), ] @@ -284,4 +284,5 @@ texinfo_documents = [ source_parsers = { '.md': CommonMarkParser, + '.lhs': CommonMarkParser, } From 7bb393fe17ed8262dd36c72e99d83a142a50a0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Mon, 25 Jan 2016 22:06:44 +0100 Subject: [PATCH 04/50] switch to rtd default theme --- tutorial/conf.py | 2 +- tutorial/index.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tutorial/index.md diff --git a/tutorial/conf.py b/tutorial/conf.py index 832c0cf8..48d677b6 100644 --- a/tutorial/conf.py +++ b/tutorial/conf.py @@ -107,7 +107,7 @@ todo_include_todos = False # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/tutorial/index.md b/tutorial/index.md new file mode 100644 index 00000000..378155e1 --- /dev/null +++ b/tutorial/index.md @@ -0,0 +1,68 @@ +Servant tutorial +================ + +This is an introductory tutorial to the current version of *servant*, which is **0.4**. Any comment or issue can be directed to [this website's issue tracker](http://github.com/haskell-servant/haskell-servant.github.io/issues). + +Github +------- + +- the servant packages: [haskell-servant/servant](https://github.com/haskell-servant/servant) +- the website (including this tutorial): [haskell-servant/haskell-servant.github.io](https://github.com/haskell-servant/haskell-servant.github.io/) +- Feel free to use the issue tracker (or to send PRs!) on the website's repository to give feedback and suggestions about this tutorial + +Introduction +------------- + +*servant* has the following guiding principles: + +- concision + + This is a pretty wide-ranging principle. You should be able to get nice + documentation for your web servers, and client libraries, without repeating + yourself. You should not have to manually serialize and deserialize your + resources, but only declare how to do those things *once per type*. If a + bunch of your handlers take the same query parameters, you shouldn't have to + repeat that logic for each handler, but instead just "apply" it to all of + them at once. Your handlers shouldn't be where composition goes to die. And + so on. + +- flexibility + + If we haven't thought of your use case, it should still be easily + achievable. If you want to use templating library X, go ahead. Forms? Do + them however you want, but without difficulty. We're not opinionated. + +- separation of concerns + + Your handlers and your HTTP logic should be separate. True to the philosphy + at the core of HTTP and REST, with *servant* your handlers return normal + Haskell datatypes - that's the resource. And then from a description of your + API, *servant* handles the *presentation* (i.e., the Content-Types). But + that's just one example. + +- type safety + + Want to be sure your API meets a specification? Your compiler can check + that for you. Links you can be sure exist? You got it. + +To stick true to these principles, we do things a little differently than you +might expect. The core idea is *reifying the description of your API*. Once +reified, everything follows. We think we might be the first web framework to +reify API descriptions in an extensible way. We're pretty sure we're the first +to reify it as *types*. + +To be able to write a webservice you only need to read the first two sections, +but the goal of this document being to get you started with servant, we also +cover the couple of ways you can extend servant for a great good. + +Tutorial +--------- + +.. toctree:: + :maxdepth: 2 + + api-type.lhs + server.lhs + client.lhs + javascript.lhs + docs.lhs From 1d4e3a1e5b88aa7b54802a7e40aa606a5126c391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Mon, 25 Jan 2016 22:30:16 +0100 Subject: [PATCH 05/50] Moved tutorial to servant-examples/tutorial and include it in doc/index.rst --- .gitignore | 2 +- {tutorial => doc}/Makefile | 0 doc/conf.py | 15 +- doc/index.rst | 3 +- {tutorial => doc}/requirements.txt | 0 doc/tutorial | 1 + servant-examples/tutorial/T8.hs | 49 --- .../tutorial}/api-type.lhs | 0 .../tutorial}/client.lhs | 0 .../tutorial}/docs.lhs | 0 .../tutorial}/index.rst | 2 +- .../tutorial}/javascript.lhs | 0 .../tutorial}/server.lhs | 0 servant-examples/tutorial/t8-main.hs | 4 - servant-examples/tutorial/t9/index.html | 26 -- servant-examples/tutorial/t9/ui.js | 61 ---- servant-examples/tutorial/tutorial.hs | 39 --- tutorial/conf.py | 288 ------------------ tutorial/index.md | 68 ----- 19 files changed, 13 insertions(+), 545 deletions(-) rename {tutorial => doc}/Makefile (100%) rename {tutorial => doc}/requirements.txt (100%) create mode 120000 doc/tutorial delete mode 100644 servant-examples/tutorial/T8.hs rename {tutorial => servant-examples/tutorial}/api-type.lhs (100%) rename {tutorial => servant-examples/tutorial}/client.lhs (100%) rename {tutorial => servant-examples/tutorial}/docs.lhs (100%) rename {tutorial => servant-examples/tutorial}/index.rst (99%) rename {tutorial => servant-examples/tutorial}/javascript.lhs (100%) rename {tutorial => servant-examples/tutorial}/server.lhs (100%) delete mode 100644 servant-examples/tutorial/t8-main.hs delete mode 100644 servant-examples/tutorial/t9/index.html delete mode 100644 servant-examples/tutorial/t9/ui.js delete mode 100644 servant-examples/tutorial/tutorial.hs delete mode 100644 tutorial/conf.py delete mode 100644 tutorial/index.md diff --git a/.gitignore b/.gitignore index ee3cbf51..f42f014b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ Setup .stack-work shell.nix default.nix -tutorial/_build +doc/_build diff --git a/tutorial/Makefile b/doc/Makefile similarity index 100% rename from tutorial/Makefile rename to doc/Makefile diff --git a/doc/conf.py b/doc/conf.py index c2761418..4e31a37d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -37,7 +37,7 @@ templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: -source_suffix = ['.md', '.rst'] +source_suffix = ['.md', '.rst', '.lhs'] # The encoding of source files. #source_encoding = 'utf-8-sig' @@ -47,8 +47,8 @@ master_doc = 'index' # General information about the project. project = u'servant' -copyright = u'2015-2016, Servant contributors' -author = u'Servant contributors' +copyright = u'2016, Servant Contributors' +author = u'Servant Contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -74,7 +74,7 @@ language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ['_build', 'venv'] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -108,7 +108,7 @@ todo_include_todos = False # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -# html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -222,8 +222,8 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'servant.tex', u'servant Documentation', - u'Servant contributors', 'manual'), + (master_doc, 'servant.tex', u'servant Documentation', + u'Servant Contributors', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -285,4 +285,5 @@ texinfo_documents = [ source_parsers = { '.md': CommonMarkParser, + '.lhs': CommonMarkParser, } diff --git a/doc/index.rst b/doc/index.rst index 41b48c52..ca7b5e5f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -5,7 +5,8 @@ Documentation table of contents ------------------------------- .. toctree:: + :maxdepth: 2 README.md + tutorial/index.rst CONTRIBUTING.md - diff --git a/tutorial/requirements.txt b/doc/requirements.txt similarity index 100% rename from tutorial/requirements.txt rename to doc/requirements.txt diff --git a/doc/tutorial b/doc/tutorial new file mode 120000 index 00000000..6072fcb4 --- /dev/null +++ b/doc/tutorial @@ -0,0 +1 @@ +../servant-examples/tutorial \ No newline at end of file diff --git a/servant-examples/tutorial/T8.hs b/servant-examples/tutorial/T8.hs deleted file mode 100644 index 4e55df6f..00000000 --- a/servant-examples/tutorial/T8.hs +++ /dev/null @@ -1,49 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} -module T8 where - -import Control.Monad.Trans.Except -import Network.HTTP.Client (Manager, defaultManagerSettings, - newManager) -import Servant -import Servant.Client -import System.IO.Unsafe (unsafePerformIO) - -import T3 - -position :: Int -- ^ value for "x" - -> Int -- ^ value for "y" - -> ExceptT ServantError IO Position - -hello :: Maybe String -- ^ an optional value for "name" - -> ExceptT ServantError IO HelloMessage - -marketing :: ClientInfo -- ^ value for the request body - -> ExceptT ServantError IO Email - -position :<|> hello :<|> marketing = client api baseUrl manager - -baseUrl :: BaseUrl -baseUrl = BaseUrl Http "localhost" 8081 "" - -{-# NOINLINE manager #-} -manager :: Manager -manager = unsafePerformIO $ newManager defaultManagerSettings - -queries :: ExceptT ServantError IO (Position, HelloMessage, Email) -queries = do - pos <- position 10 10 - msg <- hello (Just "servant") - em <- marketing (ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]) - return (pos, msg, em) - -run :: IO () -run = do - res <- runExceptT queries - case res of - Left err -> putStrLn $ "Error: " ++ show err - Right (pos, msg, em) -> do - print pos - print msg - print em diff --git a/tutorial/api-type.lhs b/servant-examples/tutorial/api-type.lhs similarity index 100% rename from tutorial/api-type.lhs rename to servant-examples/tutorial/api-type.lhs diff --git a/tutorial/client.lhs b/servant-examples/tutorial/client.lhs similarity index 100% rename from tutorial/client.lhs rename to servant-examples/tutorial/client.lhs diff --git a/tutorial/docs.lhs b/servant-examples/tutorial/docs.lhs similarity index 100% rename from tutorial/docs.lhs rename to servant-examples/tutorial/docs.lhs diff --git a/tutorial/index.rst b/servant-examples/tutorial/index.rst similarity index 99% rename from tutorial/index.rst rename to servant-examples/tutorial/index.rst index 378155e1..9044a4d5 100644 --- a/tutorial/index.rst +++ b/servant-examples/tutorial/index.rst @@ -59,7 +59,7 @@ Tutorial --------- .. toctree:: - :maxdepth: 2 + :maxdepth: 1 api-type.lhs server.lhs diff --git a/tutorial/javascript.lhs b/servant-examples/tutorial/javascript.lhs similarity index 100% rename from tutorial/javascript.lhs rename to servant-examples/tutorial/javascript.lhs diff --git a/tutorial/server.lhs b/servant-examples/tutorial/server.lhs similarity index 100% rename from tutorial/server.lhs rename to servant-examples/tutorial/server.lhs diff --git a/servant-examples/tutorial/t8-main.hs b/servant-examples/tutorial/t8-main.hs deleted file mode 100644 index b0e4979d..00000000 --- a/servant-examples/tutorial/t8-main.hs +++ /dev/null @@ -1,4 +0,0 @@ -import T8 - -main :: IO () -main = run diff --git a/servant-examples/tutorial/t9/index.html b/servant-examples/tutorial/t9/index.html deleted file mode 100644 index 7ec49c70..00000000 --- a/servant-examples/tutorial/t9/index.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - Tutorial - 9 - servant-jquery - - -

Books

- -
-

Results for ""

-
    -
-
-
-

Approximating π

-

Count: 0

-

Successes: 0

-

- - - - - - \ No newline at end of file diff --git a/servant-examples/tutorial/t9/ui.js b/servant-examples/tutorial/t9/ui.js deleted file mode 100644 index 7148827a..00000000 --- a/servant-examples/tutorial/t9/ui.js +++ /dev/null @@ -1,61 +0,0 @@ -/* book search */ -function updateResults(data) -{ - console.log(data); - $('#results').html(""); - $('#query').text("\"" + data.query + "\""); - for(var i = 0; i < data.results.length; i++) - { - $('#results').append(renderBook(data.results[i])); - } -} - -function renderBook(book) -{ - var li = '
  • ' + book.title + ', ' - + book.author + ' - ' + book.year + '
  • '; - return li; -} - -function searchBooks() -{ - var q = $('#q').val(); - getBooks(q, updateResults, console.log) -} - -searchBooks(); -$('#q').keyup(function() { - searchBooks(); -}); - -/* approximating pi */ -var count = 0; -var successes = 0; - -function f(data) -{ - var x = data.x, y = data.y; - if(x*x + y*y <= 1) - { - successes++; - } - - count++; - - update('#count', count); - update('#successes', successes); - update('#pi', 4*successes/count); -} - -function update(id, val) -{ - $(id).text(val); -} - -function refresh() -{ - getPoint(f, console.log); -} - -window.setInterval(refresh, 200); - diff --git a/servant-examples/tutorial/tutorial.hs b/servant-examples/tutorial/tutorial.hs deleted file mode 100644 index 32dc4c06..00000000 --- a/servant-examples/tutorial/tutorial.hs +++ /dev/null @@ -1,39 +0,0 @@ -import Network.Wai -import Network.Wai.Handler.Warp -import System.Environment - -import qualified T1 -import qualified T10 -import qualified T2 -import qualified T3 -import qualified T4 -import qualified T5 -import qualified T6 -import qualified T7 -import qualified T9 - -app :: String -> (Application -> IO ()) -> IO () -app n f = case n of - "1" -> f T1.app - "2" -> f T2.app - "3" -> f T3.app - "4" -> f T4.app - "5" -> f T5.app - "6" -> f T6.app - "7" -> f T7.app - "8" -> f T3.app - "9" -> T9.writeJSFiles >> f T9.app - "10" -> f T10.app - _ -> usage - -main :: IO () -main = do - args <- getArgs - case args of - [n] -> app n (run 8081) - _ -> usage - -usage :: IO () -usage = do - putStrLn "Usage:\t tutorial N" - putStrLn "\t\twhere N is the number of the example you want to run." diff --git a/tutorial/conf.py b/tutorial/conf.py deleted file mode 100644 index 48d677b6..00000000 --- a/tutorial/conf.py +++ /dev/null @@ -1,288 +0,0 @@ -# -*- coding: utf-8 -*- -# -# servant documentation build configuration file, created by -# sphinx-quickstart on Fri Jan 22 12:22:48 2016. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os -from recommonmark.parser import CommonMarkParser - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -source_suffix = ['.md', '.rst', '.lhs'] - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'servant' -copyright = u'2016, Servant Contributors' -author = u'Servant Contributors' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = u'0.1' -# The full version, including alpha/beta/rc tags. -release = u'0.1' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build', 'venv'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'servantdoc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'servant.tex', u'servant Documentation', - u'Servant Contributors', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'servant', u'servant Documentation', - [author], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'servant', u'servant Documentation', - author, 'servant', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - -source_parsers = { - '.md': CommonMarkParser, - '.lhs': CommonMarkParser, -} diff --git a/tutorial/index.md b/tutorial/index.md deleted file mode 100644 index 378155e1..00000000 --- a/tutorial/index.md +++ /dev/null @@ -1,68 +0,0 @@ -Servant tutorial -================ - -This is an introductory tutorial to the current version of *servant*, which is **0.4**. Any comment or issue can be directed to [this website's issue tracker](http://github.com/haskell-servant/haskell-servant.github.io/issues). - -Github -------- - -- the servant packages: [haskell-servant/servant](https://github.com/haskell-servant/servant) -- the website (including this tutorial): [haskell-servant/haskell-servant.github.io](https://github.com/haskell-servant/haskell-servant.github.io/) -- Feel free to use the issue tracker (or to send PRs!) on the website's repository to give feedback and suggestions about this tutorial - -Introduction -------------- - -*servant* has the following guiding principles: - -- concision - - This is a pretty wide-ranging principle. You should be able to get nice - documentation for your web servers, and client libraries, without repeating - yourself. You should not have to manually serialize and deserialize your - resources, but only declare how to do those things *once per type*. If a - bunch of your handlers take the same query parameters, you shouldn't have to - repeat that logic for each handler, but instead just "apply" it to all of - them at once. Your handlers shouldn't be where composition goes to die. And - so on. - -- flexibility - - If we haven't thought of your use case, it should still be easily - achievable. If you want to use templating library X, go ahead. Forms? Do - them however you want, but without difficulty. We're not opinionated. - -- separation of concerns - - Your handlers and your HTTP logic should be separate. True to the philosphy - at the core of HTTP and REST, with *servant* your handlers return normal - Haskell datatypes - that's the resource. And then from a description of your - API, *servant* handles the *presentation* (i.e., the Content-Types). But - that's just one example. - -- type safety - - Want to be sure your API meets a specification? Your compiler can check - that for you. Links you can be sure exist? You got it. - -To stick true to these principles, we do things a little differently than you -might expect. The core idea is *reifying the description of your API*. Once -reified, everything follows. We think we might be the first web framework to -reify API descriptions in an extensible way. We're pretty sure we're the first -to reify it as *types*. - -To be able to write a webservice you only need to read the first two sections, -but the goal of this document being to get you started with servant, we also -cover the couple of ways you can extend servant for a great good. - -Tutorial ---------- - -.. toctree:: - :maxdepth: 2 - - api-type.lhs - server.lhs - client.lhs - javascript.lhs - docs.lhs From db602e8a79ad8eee652bd3d7e2be74787d11a34a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Tue, 26 Jan 2016 00:27:07 +0100 Subject: [PATCH 06/50] tutorial: add tutorial compiling script and conversion script --- .gitignore | 1 + servant-examples/tutorial/check/check.sh | 11 +++++++++ servant-examples/tutorial/check/tinc.yaml | 15 ++++++++++++ servant-examples/tutorial/convert.hs | 30 +++++++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100755 servant-examples/tutorial/check/check.sh create mode 100644 servant-examples/tutorial/check/tinc.yaml create mode 100644 servant-examples/tutorial/convert.hs diff --git a/.gitignore b/.gitignore index f42f014b..1ea6ffef 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ Setup shell.nix default.nix doc/_build +doc/venv diff --git a/servant-examples/tutorial/check/check.sh b/servant-examples/tutorial/check/check.sh new file mode 100755 index 00000000..5425d80a --- /dev/null +++ b/servant-examples/tutorial/check/check.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -o errexit + +# tinc + +cabal exec -- ghc -Wall -Werror -outputdir build-output ../api-type.lhs -O0 -c -pgmL markdown-unlit +#cabal exec -- ghc -Wall -Werror -outputdir build-output ../server.lhs -O0 -c -fno-warn-missing-methods -fno-warn-name-shadowing +#cabal exec -- ghc -Wall -Werror -outputdir build-output ../client.lhs -O0 -c -fno-warn-missing-methods -fno-warn-name-shadowing +#cabal exec -- ghc -Wall -Werror -outputdir build-output ../javascript.lhs -O0 -c -fno-warn-missing-methods +#cabal exec -- ghc -Wall -Werror -ibuild-output -outputdir build-output ../docs.lhs -O0 -c -fno-warn-missing-methods diff --git a/servant-examples/tutorial/check/tinc.yaml b/servant-examples/tutorial/check/tinc.yaml new file mode 100644 index 00000000..2a32c412 --- /dev/null +++ b/servant-examples/tutorial/check/tinc.yaml @@ -0,0 +1,15 @@ +dependencies: + - name: servant + path: ../../../servant + - name: servant-server + path: ../../../servant-server + - name: servant-client + path: ../../../servant-client + - name: servant-js + path: ../../../servant-js + - name: servant-lucid + path: ../../../servant-lucid + - name: servant-docs + path: ../../../servant-docs + - name: servant-foreign + path: ../../../servant-foreign diff --git a/servant-examples/tutorial/convert.hs b/servant-examples/tutorial/convert.hs new file mode 100644 index 00000000..ebcca21e --- /dev/null +++ b/servant-examples/tutorial/convert.hs @@ -0,0 +1,30 @@ + +import Control.Arrow +import Data.Foldable +import Data.List +import System.Environment + +main = do + files <- getArgs + forM_ files $ \ file -> do + convertM file + +convertM :: FilePath -> IO () +convertM file = do + contents <- readFile file + seq (length contents) (return ()) + writeFile file (convert contents) + +convert :: String -> String +convert = + lines >>> + groupBy (\ a b -> take 1 a == take 1 b) >>> + map go >>> + concat >>> + unlines + where + go :: [String] -> [String] + go (a : r) + | ">" `isPrefixOf` a + = "``` haskell" : map (drop 2) (a : r) ++ "```" : [] + go x = x From 62fffed1f1e3d68d74031ee90a04d9803ac344ad Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Wed, 27 Jan 2016 22:04:13 +0100 Subject: [PATCH 07/50] Remove servant-examples --- doc/tutorial | 1 - .../tutorial/api-type.lhs | 0 .../tutorial/check/check.sh | 0 .../tutorial/check/tinc.yaml | 0 {servant-examples => doc}/tutorial/client.lhs | 0 {servant-examples => doc}/tutorial/convert.hs | 0 {servant-examples => doc}/tutorial/docs.lhs | 0 {servant-examples => doc}/tutorial/index.rst | 0 .../tutorial/javascript.lhs | 0 {servant-examples => doc}/tutorial/server.lhs | 0 servant-examples/LICENSE | 30 -- servant-examples/Setup.hs | 2 - servant-examples/hackage/hackage.hs | 90 ------ servant-examples/include/overlapping-compat.h | 8 - servant-examples/socket-io-chat/Chat.hs | 109 ------- .../socket-io-chat/resources/index.html | 28 -- .../socket-io-chat/resources/main.js | 274 ------------------ .../socket-io-chat/resources/style.css | 150 ---------- servant-examples/tinc.yaml | 15 - 19 files changed, 707 deletions(-) delete mode 120000 doc/tutorial rename {servant-examples => doc}/tutorial/api-type.lhs (100%) rename {servant-examples => doc}/tutorial/check/check.sh (100%) rename {servant-examples => doc}/tutorial/check/tinc.yaml (100%) rename {servant-examples => doc}/tutorial/client.lhs (100%) rename {servant-examples => doc}/tutorial/convert.hs (100%) rename {servant-examples => doc}/tutorial/docs.lhs (100%) rename {servant-examples => doc}/tutorial/index.rst (100%) rename {servant-examples => doc}/tutorial/javascript.lhs (100%) rename {servant-examples => doc}/tutorial/server.lhs (100%) delete mode 100644 servant-examples/LICENSE delete mode 100644 servant-examples/Setup.hs delete mode 100644 servant-examples/hackage/hackage.hs delete mode 100644 servant-examples/include/overlapping-compat.h delete mode 100644 servant-examples/socket-io-chat/Chat.hs delete mode 100644 servant-examples/socket-io-chat/resources/index.html delete mode 100644 servant-examples/socket-io-chat/resources/main.js delete mode 100644 servant-examples/socket-io-chat/resources/style.css delete mode 100644 servant-examples/tinc.yaml diff --git a/doc/tutorial b/doc/tutorial deleted file mode 120000 index 6072fcb4..00000000 --- a/doc/tutorial +++ /dev/null @@ -1 +0,0 @@ -../servant-examples/tutorial \ No newline at end of file diff --git a/servant-examples/tutorial/api-type.lhs b/doc/tutorial/api-type.lhs similarity index 100% rename from servant-examples/tutorial/api-type.lhs rename to doc/tutorial/api-type.lhs diff --git a/servant-examples/tutorial/check/check.sh b/doc/tutorial/check/check.sh similarity index 100% rename from servant-examples/tutorial/check/check.sh rename to doc/tutorial/check/check.sh diff --git a/servant-examples/tutorial/check/tinc.yaml b/doc/tutorial/check/tinc.yaml similarity index 100% rename from servant-examples/tutorial/check/tinc.yaml rename to doc/tutorial/check/tinc.yaml diff --git a/servant-examples/tutorial/client.lhs b/doc/tutorial/client.lhs similarity index 100% rename from servant-examples/tutorial/client.lhs rename to doc/tutorial/client.lhs diff --git a/servant-examples/tutorial/convert.hs b/doc/tutorial/convert.hs similarity index 100% rename from servant-examples/tutorial/convert.hs rename to doc/tutorial/convert.hs diff --git a/servant-examples/tutorial/docs.lhs b/doc/tutorial/docs.lhs similarity index 100% rename from servant-examples/tutorial/docs.lhs rename to doc/tutorial/docs.lhs diff --git a/servant-examples/tutorial/index.rst b/doc/tutorial/index.rst similarity index 100% rename from servant-examples/tutorial/index.rst rename to doc/tutorial/index.rst diff --git a/servant-examples/tutorial/javascript.lhs b/doc/tutorial/javascript.lhs similarity index 100% rename from servant-examples/tutorial/javascript.lhs rename to doc/tutorial/javascript.lhs diff --git a/servant-examples/tutorial/server.lhs b/doc/tutorial/server.lhs similarity index 100% rename from servant-examples/tutorial/server.lhs rename to doc/tutorial/server.lhs diff --git a/servant-examples/LICENSE b/servant-examples/LICENSE deleted file mode 100644 index 68d30586..00000000 --- a/servant-examples/LICENSE +++ /dev/null @@ -1,30 +0,0 @@ -Copyright (c) 2015-2016, Servant Contributors - -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 Alp Mestanogullari 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. diff --git a/servant-examples/Setup.hs b/servant-examples/Setup.hs deleted file mode 100644 index 44671092..00000000 --- a/servant-examples/Setup.hs +++ /dev/null @@ -1,2 +0,0 @@ -import Distribution.Simple -main = defaultMain diff --git a/servant-examples/hackage/hackage.hs b/servant-examples/hackage/hackage.hs deleted file mode 100644 index 4d29b556..00000000 --- a/servant-examples/hackage/hackage.hs +++ /dev/null @@ -1,90 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeOperators #-} -{-# OPTIONS_GHC -fno-warn-unused-imports #-} -import Control.Applicative -import Control.Monad -import Control.Monad.IO.Class -import Control.Monad.Trans.Except -import Data.Aeson -import Data.Monoid -import Data.Proxy -import Data.Text (Text) -import GHC.Generics -import Network.HTTP.Client (Manager, defaultManagerSettings, - newManager) -import System.IO.Unsafe (unsafePerformIO) -import Servant.API -import Servant.Client - -import qualified Data.Text as T -import qualified Data.Text.IO as T - -type HackageAPI = - "users" :> Get '[JSON] [UserSummary] - :<|> "user" :> Capture "username" Username :> Get '[JSON] UserDetailed - :<|> "packages" :> Get '[JSON] [Package] - -type Username = Text - -data UserSummary = UserSummary - { summaryUsername :: Username - , summaryUserid :: Int - } deriving (Eq, Show) - -instance FromJSON UserSummary where - parseJSON (Object o) = - UserSummary <$> o .: "username" - <*> o .: "userid" - - parseJSON _ = mzero - -type Group = Text - -data UserDetailed = UserDetailed - { username :: Username - , userid :: Int - , groups :: [Group] - } deriving (Eq, Show, Generic) - -instance FromJSON UserDetailed - -newtype Package = Package { packageName :: Text } - deriving (Eq, Show, Generic) - -instance FromJSON Package - -hackageAPI :: Proxy HackageAPI -hackageAPI = Proxy - - -{-# NOINLINE manager #-} -manager :: Manager -manager = unsafePerformIO $ newManager defaultManagerSettings - -getUsers :: ExceptT ServantError IO [UserSummary] -getUser :: Username -> ExceptT ServantError IO UserDetailed -getPackages :: ExceptT ServantError IO [Package] -getUsers :<|> getUser :<|> getPackages = - client hackageAPI (BaseUrl Http "hackage.haskell.org" 80 "") manager - -main :: IO () -main = print =<< uselessNumbers - -uselessNumbers :: IO (Either ServantError ()) -uselessNumbers = runExceptT $ do - users <- getUsers - liftIO . putStrLn $ show (length users) ++ " users" - - user <- liftIO $ do - putStrLn "Enter a valid hackage username" - T.getLine - userDetailed <- getUser user - liftIO . T.putStrLn $ user <> " maintains " <> T.pack (show (length $ groups userDetailed)) <> " packages" - - packages <- getPackages - let monadPackages = filter (isMonadPackage . packageName) packages - liftIO . putStrLn $ show (length monadPackages) ++ " monad packages" - - where isMonadPackage = T.isInfixOf "monad" diff --git a/servant-examples/include/overlapping-compat.h b/servant-examples/include/overlapping-compat.h deleted file mode 100644 index eef9d4ea..00000000 --- a/servant-examples/include/overlapping-compat.h +++ /dev/null @@ -1,8 +0,0 @@ -#if __GLASGOW_HASKELL__ >= 710 -#define OVERLAPPABLE_ {-# OVERLAPPABLE #-} -#define OVERLAPPING_ {-# OVERLAPPING #-} -#else -{-# LANGUAGE OverlappingInstances #-} -#define OVERLAPPABLE_ -#define OVERLAPPING_ -#endif diff --git a/servant-examples/socket-io-chat/Chat.hs b/servant-examples/socket-io-chat/Chat.hs deleted file mode 100644 index 9f2faa92..00000000 --- a/servant-examples/socket-io-chat/Chat.hs +++ /dev/null @@ -1,109 +0,0 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE KindSignatures #-} -{-# LANGUAGE RankNTypes #-} - -module Chat (eioServer, ServerState (..)) where - -import Prelude hiding (mapM_) - -#if !MIN_VERSION_base(4,8,0) -import Control.Applicative ((<$>), pure) -#endif -import Control.Monad.State.Class (MonadState) -import Control.Monad.IO.Class (MonadIO) -import Control.Monad.IO.Class (liftIO) -import Data.Aeson ((.=)) -import Data.Foldable (mapM_) - -import qualified Control.Concurrent.STM as STM -import qualified Data.Aeson as Aeson -import qualified Data.Text as Text -import qualified Network.SocketIO as SocketIO - - -data AddUser = AddUser Text.Text - -instance Aeson.FromJSON AddUser where - parseJSON = Aeson.withText "AddUser" $ pure . AddUser - - -data NumConnected = NumConnected !Int - -instance Aeson.ToJSON NumConnected where - toJSON (NumConnected n) = Aeson.object [ "numUsers" .= n] - - -data NewMessage = NewMessage Text.Text - -instance Aeson.FromJSON NewMessage where - parseJSON = Aeson.withText "NewMessage" $ pure . NewMessage - - -data Said = Said Text.Text Text.Text - -instance Aeson.ToJSON Said where - toJSON (Said username message) = Aeson.object - [ "username" .= username - , "message" .= message - ] - -data UserName = UserName Text.Text - -instance Aeson.ToJSON UserName where - toJSON (UserName un) = Aeson.object [ "username" .= un ] - - -data UserJoined = UserJoined Text.Text Int - -instance Aeson.ToJSON UserJoined where - toJSON (UserJoined un n) = Aeson.object - [ "username" .= un - , "numUsers" .= n - ] - - --------------------------------------------------------------------------------- -data ServerState = ServerState { ssNConnected :: STM.TVar Int } - ---server :: ServerState -> StateT SocketIO.RoutingTable Snap.Snap () -eioServer :: forall (m :: * -> *). (MonadState SocketIO.RoutingTable m, MonadIO m) => ServerState -> m () -eioServer state = do - userNameMVar <- liftIO STM.newEmptyTMVarIO - let forUserName m = liftIO (STM.atomically (STM.tryReadTMVar userNameMVar)) >>= mapM_ m - - SocketIO.on "new message" $ \(NewMessage message) -> - forUserName $ \userName -> - SocketIO.broadcast "new message" (Said userName message) - - SocketIO.on "add user" $ \(AddUser userName) -> do - n <- liftIO $ STM.atomically $ do - n <- (+ 1) <$> STM.readTVar (ssNConnected state) - STM.putTMVar userNameMVar userName - STM.writeTVar (ssNConnected state) n - return n - - SocketIO.emit "login" (NumConnected n) - SocketIO.broadcast "user joined" (UserJoined userName n) - - SocketIO.appendDisconnectHandler $ do - (n, mUserName) <- liftIO $ STM.atomically $ do - n <- (+ (-1)) <$> STM.readTVar (ssNConnected state) - mUserName <- STM.tryReadTMVar userNameMVar - STM.writeTVar (ssNConnected state) n - return (n, mUserName) - - case mUserName of - Nothing -> return () - Just userName -> - SocketIO.broadcast "user left" (UserJoined userName n) - - SocketIO.on "typing" $ - forUserName $ \userName -> - SocketIO.broadcast "typing" (UserName userName) - - SocketIO.on "stop typing" $ - forUserName $ \userName -> - SocketIO.broadcast "stop typing" (UserName userName) - diff --git a/servant-examples/socket-io-chat/resources/index.html b/servant-examples/socket-io-chat/resources/index.html deleted file mode 100644 index 92b055ff..00000000 --- a/servant-examples/socket-io-chat/resources/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - Socket.IO Chat Example - - - -
      -
    • -
      -
        -
        - -
      • - -
      - - - - - - diff --git a/servant-examples/socket-io-chat/resources/main.js b/servant-examples/socket-io-chat/resources/main.js deleted file mode 100644 index 08be0ad4..00000000 --- a/servant-examples/socket-io-chat/resources/main.js +++ /dev/null @@ -1,274 +0,0 @@ -$(function() { - var FADE_TIME = 150; // ms - var TYPING_TIMER_LENGTH = 400; // ms - var COLORS = [ - '#e21400', '#91580f', '#f8a700', '#f78b00', - '#58dc00', '#287b00', '#a8f07a', '#4ae8c4', - '#3b88eb', '#3824aa', '#a700ff', '#d300e7' - ]; - - // Initialize varibles - var $window = $(window); - var $usernameInput = $('.usernameInput'); // Input for username - var $messages = $('.messages'); // Messages area - var $inputMessage = $('.inputMessage'); // Input message input box - - var $loginPage = $('.login.page'); // The login page - var $chatPage = $('.chat.page'); // The chatroom page - - // Prompt for setting a username - var username; - var connected = false; - var typing = false; - var lastTypingTime; - var $currentInput = $usernameInput.focus(); - - var socket = io(); - - function addParticipantsMessage (data) { - var message = ''; - if (data.numUsers === 1) { - message += "there's 1 participant"; - } else { - message += "there're " + data.numUsers + " participants"; - } - log(message); - } - - // Sets the client's username - function setUsername () { - username = cleanInput($usernameInput.val().trim()); - - // If the username is valid - if (username) { - $loginPage.fadeOut(); - $chatPage.show(); - $loginPage.off('click'); - $currentInput = $inputMessage.focus(); - - // Tell the server your username - socket.emit('add user', username); - } - } - - // Sends a chat message - function sendMessage () { - var message = $inputMessage.val(); - // Prevent markup from being injected into the message - message = cleanInput(message); - // if there is a non-empty message and a socket connection - if (message && connected) { - $inputMessage.val(''); - addChatMessage({ - username: username, - message: message - }); - // tell server to execute 'new message' and send along one parameter - socket.emit('new message', message); - } - } - - // Log a message - function log (message, options) { - var $el = $('
    • ').addClass('log').text(message); - addMessageElement($el, options); - } - - // Adds the visual chat message to the message list - function addChatMessage (data, options) { - // Don't fade the message in if there is an 'X was typing' - var $typingMessages = getTypingMessages(data); - options = options || {}; - if ($typingMessages.length !== 0) { - options.fade = false; - $typingMessages.remove(); - } - - var $usernameDiv = $('') - .text(data.username) - .css('color', getUsernameColor(data.username)); - var $messageBodyDiv = $('') - .text(data.message); - - var typingClass = data.typing ? 'typing' : ''; - var $messageDiv = $('
    • ') - .data('username', data.username) - .addClass(typingClass) - .append($usernameDiv, $messageBodyDiv); - - addMessageElement($messageDiv, options); - } - - // Adds the visual chat typing message - function addChatTyping (data) { - data.typing = true; - data.message = 'is typing'; - addChatMessage(data); - } - - // Removes the visual chat typing message - function removeChatTyping (data) { - getTypingMessages(data).fadeOut(function () { - $(this).remove(); - }); - } - - // Adds a message element to the messages and scrolls to the bottom - // el - The element to add as a message - // options.fade - If the element should fade-in (default = true) - // options.prepend - If the element should prepend - // all other messages (default = false) - function addMessageElement (el, options) { - var $el = $(el); - - // Setup default options - if (!options) { - options = {}; - } - if (typeof options.fade === 'undefined') { - options.fade = true; - } - if (typeof options.prepend === 'undefined') { - options.prepend = false; - } - - // Apply options - if (options.fade) { - $el.hide().fadeIn(FADE_TIME); - } - if (options.prepend) { - $messages.prepend($el); - } else { - $messages.append($el); - } - $messages[0].scrollTop = $messages[0].scrollHeight; - } - - // Prevents input from having injected markup - function cleanInput (input) { - return $('
      ').text(input).text(); - } - - // Updates the typing event - function updateTyping () { - if (connected) { - if (!typing) { - typing = true; - socket.emit('typing'); - } - lastTypingTime = (new Date()).getTime(); - - setTimeout(function () { - var typingTimer = (new Date()).getTime(); - var timeDiff = typingTimer - lastTypingTime; - if (timeDiff >= TYPING_TIMER_LENGTH && typing) { - socket.emit('stop typing'); - typing = false; - } - }, TYPING_TIMER_LENGTH); - } - } - - // Gets the 'X is typing' messages of a user - function getTypingMessages (data) { - return $('.typing.message').filter(function (i) { - return $(this).data('username') === data.username; - }); - } - - // Gets the color of a username through our hash function - function getUsernameColor (username) { - // Compute hash code - var hash = 7; - for (var i = 0; i < username.length; i++) { - hash = username.charCodeAt(i) + (hash << 5) - hash; - } - // Calculate color - var index = Math.abs(hash % COLORS.length); - return COLORS[index]; - } - - // Keyboard events - - $window.keydown(function (event) { - // Auto-focus the current input when a key is typed - if (!(event.ctrlKey || event.metaKey || event.altKey)) { - $currentInput.focus(); - } - // When the client hits ENTER on their keyboard - if (event.which === 13) { - if (username) { - sendMessage(); - socket.emit('stop typing'); - typing = false; - } else { - setUsername(); - } - } - }); - - $inputMessage.on('input', function() { - updateTyping(); - }); - - // Click events - - // Focus input when clicking anywhere on login page - $loginPage.click(function () { - $currentInput.focus(); - }); - - // Focus input when clicking on the message input's border - $inputMessage.click(function () { - $inputMessage.focus(); - }); - - // Socket events - socket.on('connected', function (data) { - console.log('connected:', data); - }); - - // Socket events - socket.on('changes', function (data) { - console.log('changes:', data); - }); - - // Whenever the server emits 'login', log the login message - socket.on('login', function (data) { - connected = true; - // Display the welcome message - var message = "Welcome to Socket.IO Chat — "; - log(message, { - prepend: true - }); - addParticipantsMessage(data); - }); - - // Whenever the server emits 'new message', update the chat body - socket.on('new message', function (data) { - addChatMessage(data); - }); - - // Whenever the server emits 'user joined', log it in the chat body - socket.on('user joined', function (data) { - log(data.username + ' joined'); - addParticipantsMessage(data); - }); - - // Whenever the server emits 'user left', log it in the chat body - socket.on('user left', function (data) { - log(data.username + ' left'); - addParticipantsMessage(data); - removeChatTyping(data); - }); - - // Whenever the server emits 'typing', show the typing message - socket.on('typing', function (data) { - addChatTyping(data); - }); - - // Whenever the server emits 'stop typing', kill the typing message - socket.on('stop typing', function (data) { - removeChatTyping(data); - }); -}); diff --git a/servant-examples/socket-io-chat/resources/style.css b/servant-examples/socket-io-chat/resources/style.css deleted file mode 100644 index 62cbe093..00000000 --- a/servant-examples/socket-io-chat/resources/style.css +++ /dev/null @@ -1,150 +0,0 @@ -/* Fix user-agent */ - -* { - box-sizing: border-box; -} - -html { - font-weight: 300; - -webkit-font-smoothing: antialiased; -} - -html, input { - font-family: - "HelveticaNeue-Light", - "Helvetica Neue Light", - "Helvetica Neue", - Helvetica, - Arial, - "Lucida Grande", - sans-serif; -} - -html, body { - height: 100%; - margin: 0; - padding: 0; -} - -ul { - list-style: none; - word-wrap: break-word; -} - -/* Pages */ - -.pages { - height: 100%; - margin: 0; - padding: 0; - width: 100%; -} - -.page { - height: 100%; - position: absolute; - width: 100%; -} - -/* Login Page */ - -.login.page { - background-color: #000; -} - -.login.page .form { - height: 100px; - margin-top: -100px; - position: absolute; - - text-align: center; - top: 50%; - width: 100%; -} - -.login.page .form .usernameInput { - background-color: transparent; - border: none; - border-bottom: 2px solid #fff; - outline: none; - padding-bottom: 15px; - text-align: center; - width: 400px; -} - -.login.page .title { - font-size: 200%; -} - -.login.page .usernameInput { - font-size: 200%; - letter-spacing: 3px; -} - -.login.page .title, .login.page .usernameInput { - color: #fff; - font-weight: 100; -} - -/* Chat page */ - -.chat.page { - display: none; -} - -/* Font */ - -.messages { - font-size: 150%; -} - -.inputMessage { - font-size: 100%; -} - -.log { - color: gray; - font-size: 70%; - margin: 5px; - text-align: center; -} - -/* Messages */ - -.chatArea { - height: 100%; - padding-bottom: 60px; -} - -.messages { - height: 100%; - margin: 0; - overflow-y: scroll; - padding: 10px 20px 10px 20px; -} - -.message.typing .messageBody { - color: gray; -} - -.username { - float: left; - font-weight: 700; - overflow: hidden; - padding-right: 15px; - text-align: right; -} - -/* Input */ - -.inputMessage { - border: 10px solid #000; - bottom: 0; - height: 60px; - left: 0; - outline: none; - padding-left: 10px; - position: absolute; - right: 0; - width: 100%; -} diff --git a/servant-examples/tinc.yaml b/servant-examples/tinc.yaml deleted file mode 100644 index 10af8970..00000000 --- a/servant-examples/tinc.yaml +++ /dev/null @@ -1,15 +0,0 @@ -dependencies: - - name: servant - path: ../servant - - name: servant-server - path: ../servant-server - - name: servant-client - path: ../servant-client - - name: servant-js - path: ../servant-js - - name: servant-lucid - path: ../servant-lucid - - name: servant-docs - path: ../servant-docs - - name: servant-foreign - path: ../servant-foreign From 5542ce8916fff43d0ca7bae4df3e868e22a24c1d Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Wed, 27 Jan 2016 22:17:59 +0100 Subject: [PATCH 08/50] Start tutorial project --- doc/tutorial/LICENSE | 30 ++++++++++++++++++++++++++++++ doc/tutorial/Setup.hs | 2 ++ doc/tutorial/tutorial.cabal | 22 ++++++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 doc/tutorial/LICENSE create mode 100644 doc/tutorial/Setup.hs create mode 100644 doc/tutorial/tutorial.cabal diff --git a/doc/tutorial/LICENSE b/doc/tutorial/LICENSE new file mode 100644 index 00000000..fc4415bd --- /dev/null +++ b/doc/tutorial/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2016, Servant Contributors + +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 Servant Contributors 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. diff --git a/doc/tutorial/Setup.hs b/doc/tutorial/Setup.hs new file mode 100644 index 00000000..9a994af6 --- /dev/null +++ b/doc/tutorial/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal new file mode 100644 index 00000000..47fc2ebd --- /dev/null +++ b/doc/tutorial/tutorial.cabal @@ -0,0 +1,22 @@ +name: tutorial +version: 0.1.0.0 +synopsis: The servant tutorial +-- description: +homepage: http://haskell-servant.github.io/ +license: BSD3 +license-file: LICENSE +author: Servant Contributors +maintainer: haskell-servant-maintainers@googlegroups.com +-- copyright: +-- category: +build-type: Simple +-- extra-source-files: +cabal-version: >=1.10 + +library + exposed-modules: api-type.lhs + -- other-modules: + -- other-extensions: + build-depends: base >=4.8 && <4.9 + -- hs-source-dirs: + default-language: Haskell2010 From 7af73d63ea6b9f47415c919fa3ad2dbf2b396417 Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Wed, 27 Jan 2016 22:26:59 +0100 Subject: [PATCH 09/50] Explicit ignore --- doc/tutorial/api-type.lhs | 14 +++++++------- doc/tutorial/client.lhs | 2 +- doc/tutorial/convert.hs | 2 +- doc/tutorial/docs.lhs | 2 +- doc/tutorial/server.lhs | 40 +++++++++++++++++++-------------------- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/doc/tutorial/api-type.lhs b/doc/tutorial/api-type.lhs index 71c84631..7b49ec8a 100644 --- a/doc/tutorial/api-type.lhs +++ b/doc/tutorial/api-type.lhs @@ -88,7 +88,7 @@ them amounts to `/`-separating them in a URL. These 5 combinators are very similar except that they each describe a different HTTP method. This is how they're declared -``` haskell +``` haskell ignore data Delete (contentTypes :: [*]) a data Get (contentTypes :: [*]) a data Patch (contentTypes :: [*]) a @@ -116,7 +116,7 @@ The `Capture` combinator in servant takes a (type-level) string representing the "name of the variable" and a type, which indicates the type we want to decode the "captured value" to. -``` haskell +``` haskell ignore data Capture (s :: Symbol) a -- s :: Symbol just says that 's' must be a type-level string. ``` @@ -153,7 +153,7 @@ active users whereas `/users` would list them all. Here are the corresponding data type declarations: -``` haskell +``` haskell ignore data QueryParam (sym :: Symbol) a data QueryParams (sym :: Symbol) a data QueryFlag (sym :: Symbol) @@ -171,7 +171,7 @@ after *January 1st, 2005*. Corresponding data type declarations below. -``` haskell +``` haskell ignore data MatrixParam (sym :: Symbol) a data MatrixParams (sym :: Symbol) a data MatrixFlag (sym :: Symbol) @@ -206,7 +206,7 @@ Request` or `Unsupported Content Type` as appropriate. Here's the data type declaration for it: -``` haskell +``` haskell ignore data ReqBody (contentTypes :: [*]) a ``` @@ -235,7 +235,7 @@ The `Header` combinator in servant takes a type-level string for the header name and the type to which we want to decode the header's value (from some textual representation), as illustrated below: -``` haskell +``` haskell ignore data Header (sym :: Symbol) a ``` @@ -274,7 +274,7 @@ headers too. *servant* provides a `Headers` combinator that carries a list of `Header` and can be used by simply wrapping the "return type" of an endpoint with it. -``` haskell +``` haskell ignore data Headers (ls :: [*]) a ``` diff --git a/doc/tutorial/client.lhs b/doc/tutorial/client.lhs index 21779f2b..f557c413 100644 --- a/doc/tutorial/client.lhs +++ b/doc/tutorial/client.lhs @@ -83,7 +83,7 @@ Each function makes available as an argument any value that the response may dep As you can see in the code above, we just "pattern match our way" to these functions. If we try to derive less or more functions than there are endpoints in the API, we obviously get an error. The `BaseUrl` value there is just: -``` haskell +``` haskell ignore -- | URI scheme to use data Scheme = Http -- ^ http:// diff --git a/doc/tutorial/convert.hs b/doc/tutorial/convert.hs index ebcca21e..ffcb60a7 100644 --- a/doc/tutorial/convert.hs +++ b/doc/tutorial/convert.hs @@ -26,5 +26,5 @@ convert = go :: [String] -> [String] go (a : r) | ">" `isPrefixOf` a - = "``` haskell" : map (drop 2) (a : r) ++ "```" : [] + = "``` haskell ignore" : map (drop 2) (a : r) ++ "```" : [] go x = x diff --git a/doc/tutorial/docs.lhs b/doc/tutorial/docs.lhs index cb662a54..2b85b9fa 100644 --- a/doc/tutorial/docs.lhs +++ b/doc/tutorial/docs.lhs @@ -94,7 +94,7 @@ With all of this, we can derive docs for our API. *servant*'s markdown pretty printer is a function named `markdown`. -``` haskell +``` haskell ignore markdown :: API -> String ``` diff --git a/doc/tutorial/server.lhs b/doc/tutorial/server.lhs index 32f098ba..411ec1fb 100644 --- a/doc/tutorial/server.lhs +++ b/doc/tutorial/server.lhs @@ -68,7 +68,7 @@ need to have some language extensions and imports: > import qualified Data.Aeson.Parser > import qualified Text.Blaze.Html -``` haskell +``` haskell ignore {-# LANGUAGE TypeFamilies #-} ``` @@ -107,7 +107,7 @@ Nothing funny going on here. But we now can define our list of two users. Let's also write our API type. -``` haskell +``` haskell ignore type UserAPI1 = "users" :> Get '[JSON] [User] ``` @@ -324,7 +324,7 @@ decided to provide a pair of typeclasses, `FromText` and `ToText` which just let you say that you can respectively *extract* or *encode* values of some type *from*/*to* text. Here are the definitions: -``` haskell +``` haskell ignore class FromText a where fromText :: Text -> Maybe a @@ -363,7 +363,7 @@ your own. or writing the instances by hand: -``` haskell +``` haskell ignore instance FromText UserId where fromText = fmap UserId fromText @@ -394,7 +394,7 @@ The truth behind `JSON` What exactly is `JSON`? Like the 3 other content types provided out of the box by *servant*, it's a really dumb data type. -``` haskell +``` haskell ignore data JSON data PlainText data FormUrlEncoded @@ -415,7 +415,7 @@ haddocks from this link, you can see that we just have to specify use `(//) :: ByteString -> ByteString -> MediaType`. The precise way to specify the `MediaType` is to write an instance for the `Accept` class: -``` haskell +``` haskell ignore -- for reference: class Accept ctype where contentType :: Proxy ctype -> MediaType @@ -428,7 +428,7 @@ The second step is centered around the `MimeRender` and `MimeUnrender` classes. These classes just let you specify a way to respectively encode and decode values respectively into or from your content-type's representation. -``` haskell +``` haskell ignore class Accept ctype => MimeRender ctype a where mimeRender :: Proxy ctype -> a -> ByteString -- alternatively readable as: @@ -442,7 +442,7 @@ In the case of `JSON`, this is easily dealt with! For any type `a` with a `ToJSON` instance, we can render values of that type to JSON using `Data.Aeson.encode`. -``` haskell +``` haskell ignore instance ToJSON a => MimeRender JSON a where mimeRender _ = encode ``` @@ -450,7 +450,7 @@ instance ToJSON a => MimeRender JSON a where And now the `MimeUnrender` class, which lets us extract values from lazy `ByteString`s, alternatively failing with an error string. -``` haskell +``` haskell ignore class Accept ctype => MimeUnrender ctype a where mimeUnrender :: Proxy ctype -> ByteString -> Either String a -- alternatively: @@ -471,7 +471,7 @@ you are curious. This function is exactly what we need for our `MimeUnrender` instance. -``` haskell +``` haskell ignore instance FromJSON a => MimeUnrender JSON a where mimeUnrender _ = eitherDecodeLenient ``` @@ -627,7 +627,7 @@ as interfaces to databases that we interact with in `IO`; Let's recall some definitions. -``` haskell +``` haskell ignore -- from the Prelude data Either e a = Left e | Right a @@ -644,7 +644,7 @@ action that either returns an error or a result. The aforementioned `either` package is worth taking a look at. Perhaps most importantly: -``` haskell +``` haskell ignore left :: Monad m => e -> EitherT e m a ``` Allows you to return an error from your handler (whereas `return` is enough to @@ -659,7 +659,7 @@ Performing IO Another important instance from the list above is `MonadIO m => MonadIO (EitherT e m)`. [`MonadIO`](http://hackage.haskell.org/package/transformers-0.4.3.0/docs/Control-Monad-IO-Class.html) is a class from the *transformers* package defined as: -``` haskell +``` haskell ignore class Monad m => MonadIO m where liftIO :: IO a -> m a ``` @@ -688,7 +688,7 @@ error message, all you have to do is use the `left` function mentioned above and provide it with the appropriate value of type `ServantErr`, which is defined as: -``` haskell +``` haskell ignore data ServantErr = ServantErr { errHTTPCode :: Int , errReasonPhrase :: String @@ -773,7 +773,7 @@ under some path in your web API. As mentioned earlier in this document, the application". Well, servant-server provides a function to get a file and directory serving WAI application, namely: -``` haskell +``` haskell ignore -- exported by Servant and Servant.Server serveDirectory :: FilePath -> Server Raw ``` @@ -809,7 +809,7 @@ In other words: Here is our little server in action. -``` haskell +``` haskell ignore $ curl http://localhost:8081/code/T1.hs {-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeFamilies #-} @@ -918,7 +918,7 @@ We can instead factor out the `userid`: However, you have to be aware that this has an effect on the type of the corresponding `Server`: -``` haskell +``` haskell ignore Server UserAPI3 = (Int -> EitherT ServantErr IO User) :<|> (Int -> EitherT ServantErr IO ()) @@ -1076,7 +1076,7 @@ Using another monad for your handlers Remember how `Server` turns combinators for HTTP methods into `EitherT ServantErr IO`? Well, actually, there's more to that. `Server` is actually a simple type synonym. -``` haskell +``` haskell ignore type Server api = ServerT api (EitherT ServantErr IO) ``` @@ -1090,7 +1090,7 @@ Natural transformations If we have a function that gets us from an `m a` to an `n a`, for any `a`, what do we have? -``` haskell +``` haskell ignore newtype m :~> n = Nat { unNat :: forall a. m a -> n a} -- For example @@ -1103,7 +1103,7 @@ So if you want to write handlers using another monad/type than `EitherT ServantErr IO`, say the `Reader String` monad, the first thing you have to prepare is a function: -``` haskell +``` haskell ignore readerToEither :: Reader String :~> EitherT ServantErr IO ``` From 8b1bf02af843ef603a746191e87504ed89d02763 Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Wed, 27 Jan 2016 22:28:58 +0100 Subject: [PATCH 10/50] Remove bird-tracks --- doc/tutorial/api-type.lhs | 136 +++--- doc/tutorial/client.lhs | 158 +++--- doc/tutorial/docs.lhs | 186 +++---- doc/tutorial/javascript.lhs | 208 ++++---- doc/tutorial/server.lhs | 938 +++++++++++++++++++----------------- 5 files changed, 886 insertions(+), 740 deletions(-) diff --git a/doc/tutorial/api-type.lhs b/doc/tutorial/api-type.lhs index 7b49ec8a..fbf42644 100644 --- a/doc/tutorial/api-type.lhs +++ b/doc/tutorial/api-type.lhs @@ -6,13 +6,15 @@ toc: true The source for this tutorial section is a literate haskell file, so first we need to have some language extensions and imports: -> {-# LANGUAGE DataKinds #-} -> {-# LANGUAGE TypeOperators #-} -> -> module ApiType where -> -> import Data.Text -> import Servant.API +``` haskell +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeOperators #-} + +module ApiType where + +import Data.Text +import Servant.API +``` Consider the following informal specification of an API: @@ -29,14 +31,16 @@ getting some client libraries, and documentation (and in the future, who knows How would we describe it with servant? As mentioned earlier, an endpoint description is a good old Haskell **type**: -> type UserAPI = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User] -> -> data SortBy = Age | Name -> -> data User = User { -> name :: String, -> age :: Int -> } +``` haskell +type UserAPI = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User] + +data SortBy = Age | Name + +data User = User { + name :: String, + age :: Int +} +``` Let's break that down: @@ -61,8 +65,10 @@ equivalent to `/`, but sometimes it just lets you chain another combinator. We can also describe APIs with multiple endpoints by using the `:<|>` combinators. Here's an example: -> type UserAPI2 = "users" :> "list-all" :> Get '[JSON] [User] -> :<|> "list-all" :> "users" :> Get '[JSON] [User] +``` haskell +type UserAPI2 = "users" :> "list-all" :> Get '[JSON] [User] + :<|> "list-all" :> "users" :> Get '[JSON] [User] +``` *servant* provides a fair amount of combinators out-of-the-box, but you can always write your own when you need it. Here's a quick overview of all the @@ -78,9 +84,11 @@ As you've already seen, you can use type-level strings (enabled with the `DataKinds` language extension) for static path fragments. Chaining them amounts to `/`-separating them in a URL. -> type UserAPI3 = "users" :> "list-all" :> "now" :> Get '[JSON] [User] -> -- describes an endpoint reachable at: -> -- /users/list-all/now +``` haskell +type UserAPI3 = "users" :> "list-all" :> "now" :> Get '[JSON] [User] + -- describes an endpoint reachable at: + -- /users/list-all/now +``` `Delete`, `Get`, `Patch`, `Post` and `Put` ------------------------------------------ @@ -99,8 +107,10 @@ data Put (contentTypes :: [*]) a An endpoint ends with one of the 5 combinators above (unless you write your own). Examples: -> type UserAPI4 = "users" :> Get '[JSON] [User] -> :<|> "admins" :> Get '[JSON] [User] +``` haskell +type UserAPI4 = "users" :> Get '[JSON] [User] + :<|> "admins" :> Get '[JSON] [User] +``` `Capture` --------- @@ -127,13 +137,15 @@ class, which the captured value must be an instance of. Examples: -> type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User -> -- equivalent to 'GET /user/:userid' -> -- except that we explicitly say that "userid" -> -- must be an integer -> -> :<|> "user" :> Capture "userid" Integer :> Delete '[] () -> -- equivalent to 'DELETE /user/:userid' +``` haskell +type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User + -- equivalent to 'GET /user/:userid' + -- except that we explicitly say that "userid" + -- must be an integer + + :<|> "user" :> Capture "userid" Integer :> Delete '[] () + -- equivalent to 'DELETE /user/:userid' +``` `QueryParam`, `QueryParams`, `QueryFlag`, `MatrixParam`, `MatrixParams` and `MatrixFlag` ---------------------------------------------------------------------------------------- @@ -179,11 +191,13 @@ data MatrixFlag (sym :: Symbol) Examples: -> type UserAPI6 = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User] -> -- equivalent to 'GET /users?sortby={age, name}' -> -> :<|> "users" :> MatrixParam "sortby" SortBy :> Get '[JSON] [User] -> -- equivalent to 'GET /users;sortby={age, name}' +``` haskell +type UserAPI6 = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User] + -- equivalent to 'GET /users?sortby={age, name}' + + :<|> "users" :> MatrixParam "sortby" SortBy :> Get '[JSON] [User] + -- equivalent to 'GET /users;sortby={age, name}' +``` Again, your handlers don't have to deserialize these things (into, for example, a `SortBy`). *servant* takes care of it. @@ -212,17 +226,19 @@ data ReqBody (contentTypes :: [*]) a Examples: -> type UserAPI7 = "users" :> ReqBody '[JSON] User :> Post '[JSON] User -> -- - equivalent to 'POST /users' with a JSON object -> -- describing a User in the request body -> -- - returns a User encoded in JSON -> -> :<|> "users" :> Capture "userid" Integer -> :> ReqBody '[JSON] User -> :> Put '[JSON] User -> -- - equivalent to 'PUT /users/:userid' with a JSON -> -- object describing a User in the request body -> -- - returns a User encoded in JSON +``` haskell +type UserAPI7 = "users" :> ReqBody '[JSON] User :> Post '[JSON] User + -- - equivalent to 'POST /users' with a JSON object + -- describing a User in the request body + -- - returns a User encoded in JSON + + :<|> "users" :> Capture "userid" Integer + :> ReqBody '[JSON] User + :> Put '[JSON] User + -- - equivalent to 'PUT /users/:userid' with a JSON + -- object describing a User in the request body + -- - returns a User encoded in JSON +``` Request `Header`s ----------------- @@ -243,7 +259,9 @@ Here's an example where we declare that an endpoint makes use of the `User-Agent` header which specifies the name of the software/library used by the client to send the request. -> type UserAPI8 = "users" :> Header "User-Agent" Text :> Get '[JSON] [User] +``` haskell +type UserAPI8 = "users" :> Header "User-Agent" Text :> Get '[JSON] [User] +``` Content types ------------- @@ -257,7 +275,9 @@ Four content-types are provided out-of-the-box by the core *servant* package: reason you wanted one of your endpoints to make your user data available under those 4 formats, you would write the API type as below: -> type UserAPI9 = "users" :> Get '[JSON, PlainText, FormUrlEncoded, OctetStream] [User] +``` haskell +type UserAPI9 = "users" :> Get '[JSON, PlainText, FormUrlEncoded, OctetStream] [User] +``` We also provide an HTML content-type, but since there's no single library that everyone uses, we decided to release 2 packages, *servant-lucid* and @@ -281,7 +301,9 @@ data Headers (ls :: [*]) a If you want to describe an endpoint that returns a "User-Count" header in each response, you could write it as below: -> type UserAPI10 = "users" :> Get '[JSON] (Headers '[Header "User-Count" Integer] [User]) +``` haskell +type UserAPI10 = "users" :> Get '[JSON] (Headers '[Header "User-Count" Integer] [User]) +``` Interoperability with other WAI `Application`s: `Raw` ----------------------------------------------------- @@ -290,14 +312,16 @@ Finally, we also include a combinator named `Raw` that can be used for two reaso - You want to serve static files from a given directory. In that case you can just say: -> type UserAPI11 = "users" :> Get '[JSON] [User] -> -- a /users endpoint -> -> :<|> Raw -> -- requests to anything else than /users -> -- go here, where the server will try to -> -- find a file with the right name -> -- at the right path +``` haskell +type UserAPI11 = "users" :> Get '[JSON] [User] + -- a /users endpoint + + :<|> Raw + -- requests to anything else than /users + -- go here, where the server will try to + -- find a file with the right name + -- at the right path +``` - You more generally want to plug a [WAI `Application`](http://hackage.haskell.org/package/wai) into your webservice. Static file serving is a specific example of that. The API type would look the diff --git a/doc/tutorial/client.lhs b/doc/tutorial/client.lhs index f557c413..9571ec8c 100644 --- a/doc/tutorial/client.lhs +++ b/doc/tutorial/client.lhs @@ -11,75 +11,85 @@ and friends. By *derive*, we mean that there's no code generation involved, the The source for this tutorial section is a literate haskell file, so first we need to have some language extensions and imports: -> {-# LANGUAGE DataKinds #-} -> {-# LANGUAGE DeriveGeneric #-} -> {-# LANGUAGE TypeOperators #-} -> -> module Client where -> -> import Control.Monad.Trans.Either -> import Data.Aeson -> import Data.Proxy -> import GHC.Generics -> import Servant.API -> import Servant.Client +``` haskell +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE TypeOperators #-} + +module Client where + +import Control.Monad.Trans.Either +import Data.Aeson +import Data.Proxy +import GHC.Generics +import Servant.API +import Servant.Client +``` Also, we need examples for some domain specific data types: -> data Position = Position -> { x :: Int -> , y :: Int -> } deriving (Show, Generic) -> -> instance FromJSON Position -> -> newtype HelloMessage = HelloMessage { msg :: String } -> deriving (Show, Generic) -> -> instance FromJSON HelloMessage -> -> data ClientInfo = ClientInfo -> { clientName :: String -> , clientEmail :: String -> , clientAge :: Int -> , clientInterestedIn :: [String] -> } deriving Generic -> -> instance ToJSON ClientInfo -> -> data Email = Email -> { from :: String -> , to :: String -> , subject :: String -> , body :: String -> } deriving (Show, Generic) -> -> instance FromJSON Email +``` haskell +data Position = Position + { x :: Int + , y :: Int + } deriving (Show, Generic) + +instance FromJSON Position + +newtype HelloMessage = HelloMessage { msg :: String } + deriving (Show, Generic) + +instance FromJSON HelloMessage + +data ClientInfo = ClientInfo + { clientName :: String + , clientEmail :: String + , clientAge :: Int + , clientInterestedIn :: [String] + } deriving Generic + +instance ToJSON ClientInfo + +data Email = Email + { from :: String + , to :: String + , subject :: String + , body :: String + } deriving (Show, Generic) + +instance FromJSON Email +``` Enough chitchat, let's see an example. Consider the following API type from the previous section: -> type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position -> :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage -> :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email +``` haskell +type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position + :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage + :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email +``` What we are going to get with *servant-client* here is 3 functions, one to query each endpoint: -> position :: Int -- ^ value for "x" -> -> Int -- ^ value for "y" -> -> EitherT ServantError IO Position -> -> hello :: Maybe String -- ^ an optional value for "name" -> -> EitherT ServantError IO HelloMessage -> -> marketing :: ClientInfo -- ^ value for the request body -> -> EitherT ServantError IO Email +``` haskell +position :: Int -- ^ value for "x" + -> Int -- ^ value for "y" + -> EitherT ServantError IO Position + +hello :: Maybe String -- ^ an optional value for "name" + -> EitherT ServantError IO HelloMessage + +marketing :: ClientInfo -- ^ value for the request body + -> EitherT ServantError IO Email +``` Each function makes available as an argument any value that the response may depend on, as evidenced in the API type. How do we get these functions? Just give a `Proxy` to your API and a host to make the requests to: -> api :: Proxy API -> api = Proxy -> -> position :<|> hello :<|> marketing = client api (BaseUrl Http "localhost" 8081) +``` haskell +api :: Proxy API +api = Proxy + +position :<|> hello :<|> marketing = client api (BaseUrl Http "localhost" 8081) +``` As you can see in the code above, we just "pattern match our way" to these functions. If we try to derive less or more functions than there are endpoints in the API, we obviously get an error. The `BaseUrl` value there is just: @@ -101,22 +111,24 @@ data BaseUrl = BaseUrl That's it. Let's now write some code that uses our client functions. -> queries :: EitherT ServantError IO (Position, HelloMessage, Email) -> queries = do -> pos <- position 10 10 -> msg <- hello (Just "servant") -> em <- marketing (ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]) -> return (pos, msg, em) -> -> run :: IO () -> run = do -> res <- runEitherT queries -> case res of -> Left err -> putStrLn $ "Error: " ++ show err -> Right (pos, msg, em) -> do -> print pos -> print msg -> print em +``` haskell +queries :: EitherT ServantError IO (Position, HelloMessage, Email) +queries = do + pos <- position 10 10 + msg <- hello (Just "servant") + em <- marketing (ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]) + return (pos, msg, em) + +run :: IO () +run = do + res <- runEitherT queries + case res of + Left err -> putStrLn $ "Error: " ++ show err + Right (pos, msg, em) -> do + print pos + print msg + print em +``` You can now run `dist/build/tutorial/tutorial 8` (the server) and `dist/build/t8-main/t8-main` (the client) to see them both in action. diff --git a/doc/tutorial/docs.lhs b/doc/tutorial/docs.lhs index 2b85b9fa..1ae4570e 100644 --- a/doc/tutorial/docs.lhs +++ b/doc/tutorial/docs.lhs @@ -6,89 +6,99 @@ toc: true The source for this tutorial section is a literate haskell file, so first we need to have some language extensions and imports: -> {-# LANGUAGE DataKinds #-} -> {-# LANGUAGE DeriveGeneric #-} -> {-# LANGUAGE FlexibleInstances #-} -> {-# LANGUAGE MultiParamTypeClasses #-} -> {-# LANGUAGE OverloadedStrings #-} -> {-# LANGUAGE TypeOperators #-} -> {-# OPTIONS_GHC -fno-warn-orphans #-} -> -> module Docs where -> -> import Data.ByteString.Lazy (ByteString) -> import Data.Proxy -> import Data.Text.Lazy.Encoding (encodeUtf8) -> import Data.Text.Lazy (pack) -> import Network.HTTP.Types -> import Network.Wai -> import Servant.API -> import Servant.Docs -> import Servant.Server +``` haskell +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeOperators #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Docs where + +import Data.ByteString.Lazy (ByteString) +import Data.Proxy +import Data.Text.Lazy.Encoding (encodeUtf8) +import Data.Text.Lazy (pack) +import Network.HTTP.Types +import Network.Wai +import Servant.API +import Servant.Docs +import Servant.Server +``` And we'll import some things from one of our earlier modules ([Serving an API](/tutorial/server.html)): -> import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..), -> server3, emailForClient) +``` haskell +import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..), + server3, emailForClient) +``` Like client function generation, documentation generation amounts to inspecting the API type and extracting all the data we need to then present it in some format to users of your API. This time however, we have to assist *servant*. While it is able to deduce a lot of things about our API, it can't magically come up with descriptions of the various pieces of our APIs that are human-friendly and explain what's going on "at the business-logic level". A good example to study for documentation generation is our webservice with the `/position`, `/hello` and `/marketing` endpoints from earlier: -> type ExampleAPI = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position -> :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage -> :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email -> -> exampleAPI :: Proxy ExampleAPI -> exampleAPI = Proxy +``` haskell +type ExampleAPI = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position + :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage + :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email + +exampleAPI :: Proxy ExampleAPI +exampleAPI = Proxy +``` While *servant* can see e.g. that there are 3 endpoints and that the response bodies will be in JSON, it doesn't know what influence the captures, parameters, request bodies and other combinators have on the webservice. This is where some manual work is required. For every capture, request body, response body, query param, we have to give some explanations about how it influences the response, what values are possible and the likes. Here's how it looks like for the parameters we have above. -> instance ToCapture (Capture "x" Int) where -> toCapture _ = -> DocCapture "x" -- name -> "(integer) position on the x axis" -- description -> -> instance ToCapture (Capture "y" Int) where -> toCapture _ = -> DocCapture "y" -- name -> "(integer) position on the y axis" -- description -> -> instance ToSample Position Position where -> toSample _ = Just (Position 3 14) -- example of output -> -> instance ToParam (QueryParam "name" String) where -> toParam _ = -> DocQueryParam "name" -- name -> ["Alp", "John Doe", "..."] -- example of values (not necessarily exhaustive) -> "Name of the person to say hello to." -- description -> Normal -- Normal, List or Flag -> -> instance ToSample HelloMessage HelloMessage where -> toSamples _ = -> [ ("When a value is provided for 'name'", HelloMessage "Hello, Alp") -> , ("When 'name' is not specified", HelloMessage "Hello, anonymous coward") -> ] -> -- mutliple examples to display this time -> -> ci :: ClientInfo -> ci = ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"] -> -> instance ToSample ClientInfo ClientInfo where -> toSample _ = Just ci -> -> instance ToSample Email Email where -> toSample _ = Just (emailForClient ci) +``` haskell +instance ToCapture (Capture "x" Int) where + toCapture _ = + DocCapture "x" -- name + "(integer) position on the x axis" -- description + +instance ToCapture (Capture "y" Int) where + toCapture _ = + DocCapture "y" -- name + "(integer) position on the y axis" -- description + +instance ToSample Position Position where + toSample _ = Just (Position 3 14) -- example of output + +instance ToParam (QueryParam "name" String) where + toParam _ = + DocQueryParam "name" -- name + ["Alp", "John Doe", "..."] -- example of values (not necessarily exhaustive) + "Name of the person to say hello to." -- description + Normal -- Normal, List or Flag + +instance ToSample HelloMessage HelloMessage where + toSamples _ = + [ ("When a value is provided for 'name'", HelloMessage "Hello, Alp") + , ("When 'name' is not specified", HelloMessage "Hello, anonymous coward") + ] + -- mutliple examples to display this time + +ci :: ClientInfo +ci = ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"] + +instance ToSample ClientInfo ClientInfo where + toSample _ = Just ci + +instance ToSample Email Email where + toSample _ = Just (emailForClient ci) +``` Types that are used as request or response bodies have to instantiate the `ToSample` typeclass which lets you specify one or more examples of values. `Capture`s and `QueryParam`s have to instantiate their respective `ToCapture` and `ToParam` classes and provide a name and some information about the concrete meaning of that argument, as illustrated in the code above. With all of this, we can derive docs for our API. -> apiDocs :: API -> apiDocs = docs exampleAPI +``` haskell +apiDocs :: API +apiDocs = docs exampleAPI +``` `API` is a type provided by *servant-docs* that stores all the information one needs about a web API in order to generate documentation in some format. Out of the box, *servant-docs* only provides a pretty documentation printer that outputs [Markdown](http://en.wikipedia.org/wiki/Markdown), but the [servant-pandoc](http://hackage.haskell.org/package/servant-pandoc) package can be used to target many useful formats. @@ -192,33 +202,37 @@ That lets us see what our API docs look down in markdown, by looking at `markdow However, we can also add one or more introduction sections to the document. We just need to tweak the way we generate `apiDocs`. We will also convert the content to a lazy `ByteString` since this is what *wai* expects for `Raw` endpoints. -> docsBS :: ByteString -> docsBS = encodeUtf8 -> . pack -> . markdown -> $ docsWithIntros [intro] exampleAPI -> -> where intro = DocIntro "Welcome" ["This is our super webservice's API.", "Enjoy!"] +``` haskell +docsBS :: ByteString +docsBS = encodeUtf8 + . pack + . markdown + $ docsWithIntros [intro] exampleAPI + + where intro = DocIntro "Welcome" ["This is our super webservice's API.", "Enjoy!"] +``` `docsWithIntros` just takes an additional parameter, a list of `DocIntro`s that must be displayed before any endpoint docs. We can now serve the API *and* the API docs with a simple server. -> type DocsAPI = ExampleAPI :<|> Raw -> -> api :: Proxy DocsAPI -> api = Proxy -> -> server :: Server DocsAPI -> server = Server.server3 :<|> serveDocs -> -> where serveDocs _ respond = -> respond $ responseLBS ok200 [plain] docsBS -> -> plain = ("Content-Type", "text/plain") -> -> app :: Application -> app = serve api server +``` haskell +type DocsAPI = ExampleAPI :<|> Raw + +api :: Proxy DocsAPI +api = Proxy + +server :: Server DocsAPI +server = Server.server3 :<|> serveDocs + + where serveDocs _ respond = + respond $ responseLBS ok200 [plain] docsBS + + plain = ("Content-Type", "text/plain") + +app :: Application +app = serve api server +``` And if you spin up this server with `dist/build/tutorial/tutorial 10` and go to anywhere else than `/position`, `/hello` and `/marketing`, you will see the API docs in markdown. This is because `serveDocs` is attempted if the 3 other endpoints don't match and systematically succeeds since its definition is to just return some fixed bytestring with the `text/plain` content type. diff --git a/doc/tutorial/javascript.lhs b/doc/tutorial/javascript.lhs index 33b4f73b..9098fe8d 100644 --- a/doc/tutorial/javascript.lhs +++ b/doc/tutorial/javascript.lhs @@ -22,117 +22,131 @@ query your API. The source for this tutorial section is a literate haskell file, so first we need to have some language extensions and imports: -> {-# LANGUAGE DataKinds #-} -> {-# LANGUAGE DeriveGeneric #-} -> {-# LANGUAGE OverloadedStrings #-} -> {-# LANGUAGE TypeOperators #-} -> -> module Javascript where -> -> import Control.Monad.IO.Class -> import Data.Aeson -> import Data.Proxy -> import Data.Text (Text) -> import qualified Data.Text as T -> import GHC.Generics -> import Language.Javascript.JQuery -> import Network.Wai -> import Servant -> import Servant.JQuery -> import System.Random +``` haskell +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeOperators #-} + +module Javascript where + +import Control.Monad.IO.Class +import Data.Aeson +import Data.Proxy +import Data.Text (Text) +import qualified Data.Text as T +import GHC.Generics +import Language.Javascript.JQuery +import Network.Wai +import Servant +import Servant.JQuery +import System.Random +``` Now let's have the API type(s) and the accompanying datatypes. -> type API = "point" :> Get '[JSON] Point -> :<|> "books" :> QueryParam "q" Text :> Get '[JSON] (Search Book) -> -> type API' = API :<|> Raw -> -> data Point = Point -> { x :: Double -> , y :: Double -> } deriving Generic -> -> instance ToJSON Point -> -> data Search a = Search -> { query :: Text -> , results :: [a] -> } deriving Generic -> -> mkSearch :: Text -> [a] -> Search a -> mkSearch = Search -> -> instance ToJSON a => ToJSON (Search a) -> -> data Book = Book -> { author :: Text -> , title :: Text -> , year :: Int -> } deriving Generic -> -> instance ToJSON Book -> -> book :: Text -> Text -> Int -> Book -> book = Book +``` haskell +type API = "point" :> Get '[JSON] Point + :<|> "books" :> QueryParam "q" Text :> Get '[JSON] (Search Book) + +type API' = API :<|> Raw + +data Point = Point + { x :: Double + , y :: Double + } deriving Generic + +instance ToJSON Point + +data Search a = Search + { query :: Text + , results :: [a] + } deriving Generic + +mkSearch :: Text -> [a] -> Search a +mkSearch = Search + +instance ToJSON a => ToJSON (Search a) + +data Book = Book + { author :: Text + , title :: Text + , year :: Int + } deriving Generic + +instance ToJSON Book + +book :: Text -> Text -> Int -> Book +book = Book +``` We need a "book database". For the purpose of this guide, let's restrict ourselves to the following books. -> books :: [Book] -> books = -> [ book "Paul Hudak" "The Haskell School of Expression: Learning Functional Programming through Multimedia" 2000 -> , book "Bryan O'Sullivan, Don Stewart, and John Goerzen" "Real World Haskell" 2008 -> , book "Miran Lipovača" "Learn You a Haskell for Great Good!" 2011 -> , book "Graham Hutton" "Programming in Haskell" 2007 -> , book "Simon Marlow" "Parallel and Concurrent Programming in Haskell" 2013 -> , book "Richard Bird" "Introduction to Functional Programming using Haskell" 1998 -> ] +``` haskell +books :: [Book] +books = + [ book "Paul Hudak" "The Haskell School of Expression: Learning Functional Programming through Multimedia" 2000 + , book "Bryan O'Sullivan, Don Stewart, and John Goerzen" "Real World Haskell" 2008 + , book "Miran Lipovača" "Learn You a Haskell for Great Good!" 2011 + , book "Graham Hutton" "Programming in Haskell" 2007 + , book "Simon Marlow" "Parallel and Concurrent Programming in Haskell" 2013 + , book "Richard Bird" "Introduction to Functional Programming using Haskell" 1998 + ] +``` Now, given an optional search string `q`, we want to perform a case insensitive search in that list of books. We're obviously not going to try and implement the best possible algorithm, this is out of scope for this tutorial. The following simple linear scan will do, given how small our list is. -> searchBook :: Monad m => Maybe Text -> m (Search Book) -> searchBook Nothing = return (mkSearch "" books) -> searchBook (Just q) = return (mkSearch q books') -> -> where books' = filter (\b -> q' `T.isInfixOf` T.toLower (author b) -> || q' `T.isInfixOf` T.toLower (title b) -> ) -> books -> q' = T.toLower q +``` haskell +searchBook :: Monad m => Maybe Text -> m (Search Book) +searchBook Nothing = return (mkSearch "" books) +searchBook (Just q) = return (mkSearch q books') + + where books' = filter (\b -> q' `T.isInfixOf` T.toLower (author b) + || q' `T.isInfixOf` T.toLower (title b) + ) + books + q' = T.toLower q +``` We also need an endpoint that generates random points `(x, y)` with `-1 <= x,y <= 1`. The code below uses [random](http://hackage.haskell.org/package/random)'s `System.Random`. -> randomPoint :: MonadIO m => m Point -> randomPoint = liftIO . getStdRandom $ \g -> -> let (rx, g') = randomR (-1, 1) g -> (ry, g'') = randomR (-1, 1) g' -> in (Point rx ry, g'') +``` haskell +randomPoint :: MonadIO m => m Point +randomPoint = liftIO . getStdRandom $ \g -> + let (rx, g') = randomR (-1, 1) g + (ry, g'') = randomR (-1, 1) g' + in (Point rx ry, g'') +``` If we add static file serving, our server is now complete. -> api :: Proxy API -> api = Proxy -> -> api' :: Proxy API' -> api' = Proxy -> -> server :: Server API -> server = randomPoint -> :<|> searchBook -> -> server' :: Server API' -> server' = server -> :<|> serveDirectory "tutorial/t9" -> -> app :: Application -> app = serve api' server' +``` haskell +api :: Proxy API +api = Proxy + +api' :: Proxy API' +api' = Proxy + +server :: Server API +server = randomPoint + :<|> searchBook + +server' :: Server API' +server' = server + :<|> serveDirectory "tutorial/t9" + +app :: Application +app = serve api' server' +``` Why two different API types, proxies and servers though? Simply because we don't want to generate javascript functions for the `Raw` part of our API type, so we need a `Proxy` for our API type `API'` without its `Raw` endpoint. Very similarly to how one can derive haskell functions, we can derive the javascript with just a simple function call to `jsForAPI` from `Servant.JQuery`. -> apiJS :: String -> apiJS = jsForAPI api +``` haskell +apiJS :: String +apiJS = jsForAPI api +``` This `String` contains 2 Javascript functions: @@ -161,11 +175,13 @@ function getbooks(q, onSuccess, onError) Right before starting up our server, we will need to write this `String` to a file, say `api.js`, along with a copy of the *jQuery* library, as provided by the [js-jquery](http://hackage.haskell.org/package/js-jquery) package. -> writeJSFiles :: IO () -> writeJSFiles = do -> writeFile "getting-started/gs9/api.js" apiJS -> jq <- readFile =<< Language.Javascript.JQuery.file -> writeFile "getting-started/gs9/jq.js" jq +``` haskell +writeJSFiles :: IO () +writeJSFiles = do + writeFile "getting-started/gs9/api.js" apiJS + jq <- readFile =<< Language.Javascript.JQuery.file + writeFile "getting-started/gs9/jq.js" jq +``` And we're good to go. Start the server with `dist/build/tutorial/tutorial 9` and go to `http://localhost:8081/`. Start typing in the name of one of the authors in our database or part of a book title, and check out how long it takes to approximate π using the method mentioned above. diff --git a/doc/tutorial/server.lhs b/doc/tutorial/server.lhs index 411ec1fb..29ca6cb8 100644 --- a/doc/tutorial/server.lhs +++ b/doc/tutorial/server.lhs @@ -34,39 +34,41 @@ Equipped with some basic knowledge about the way we represent API, let's now wri The source for this tutorial section is a literate haskell file, so first we need to have some language extensions and imports: -> {-# LANGUAGE DataKinds #-} -> {-# LANGUAGE DeriveGeneric #-} -> {-# LANGUAGE FlexibleInstances #-} -> {-# LANGUAGE GeneralizedNewtypeDeriving #-} -> {-# LANGUAGE MultiParamTypeClasses #-} -> {-# LANGUAGE OverloadedStrings #-} -> {-# LANGUAGE ScopedTypeVariables #-} -> {-# LANGUAGE TypeOperators #-} -> -> module Server where -> -> import Control.Monad.IO.Class -> import Control.Monad.Reader -> import Control.Monad.Trans.Either -> import Data.Aeson -> import Data.Aeson.Types -> import Data.Attoparsec.ByteString -> import Data.ByteString (ByteString) -> import Data.Int -> import Data.List -> import Data.String.Conversions -> import Data.Time.Calendar -> import GHC.Generics -> import Lucid -> import Network.HTTP.Media ((//), (/:)) -> import Network.Wai -> import Network.Wai.Handler.Warp -> import Servant -> import System.Directory -> import Text.Blaze -> import Text.Blaze.Html.Renderer.Utf8 -> import qualified Data.Aeson.Parser -> import qualified Text.Blaze.Html +``` haskell +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeOperators #-} + +module Server where + +import Control.Monad.IO.Class +import Control.Monad.Reader +import Control.Monad.Trans.Either +import Data.Aeson +import Data.Aeson.Types +import Data.Attoparsec.ByteString +import Data.ByteString (ByteString) +import Data.Int +import Data.List +import Data.String.Conversions +import Data.Time.Calendar +import GHC.Generics +import Lucid +import Network.HTTP.Media ((//), (/:)) +import Network.Wai +import Network.Wai.Handler.Warp +import Servant +import System.Directory +import Text.Blaze +import Text.Blaze.Html.Renderer.Utf8 +import qualified Data.Aeson.Parser +import qualified Text.Blaze.Html +``` ``` haskell ignore {-# LANGUAGE TypeFamilies #-} @@ -76,7 +78,9 @@ need to have some language extensions and imports: We will write a server that will serve the following API. -> type UserAPI1 = "users" :> Get '[JSON] [User] +``` haskell +type UserAPI1 = "users" :> Get '[JSON] [User] +``` Here's what we would like to see when making a GET request to `/users`. @@ -88,22 +92,26 @@ Here's what we would like to see when making a GET request to `/users`. Now let's define our `User` data type and write some instances for it. -> data User = User -> { name :: String -> , age :: Int -> , email :: String -> , registration_date :: Day -> } deriving (Eq, Show, Generic) -> -> instance ToJSON User +``` haskell +data User = User + { name :: String + , age :: Int + , email :: String + , registration_date :: Day + } deriving (Eq, Show, Generic) + +instance ToJSON User +``` Nothing funny going on here. But we now can define our list of two users. -> users1 :: [User] -> users1 = -> [ User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1) -> , User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1) -> ] +``` haskell +users1 :: [User] +users1 = + [ User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1) + , User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1) + ] +``` Let's also write our API type. @@ -129,27 +137,33 @@ HTTP method combinator used for the corresponding endpoint. In our case, it means we must provide a handler of type `EitherT ServantErr IO [User]`. Well, we have a monad, let's just `return` our list: -> server1 :: Server UserAPI1 -> server1 = return users1 +``` haskell +server1 :: Server UserAPI1 +server1 = return users1 +``` That's it. Now we can turn `server` into an actual webserver using [wai](http://hackage.haskell.org/package/wai) and [warp](http://hackage.haskell.org/package/warp): -> userAPI :: Proxy UserAPI1 -> userAPI = Proxy -> -> -- 'serve' comes from servant and hands you a WAI Application, -> -- which you can think of as an "abstract" web application, -> -- not yet a webserver. -> app1 :: Application -> app1 = serve userAPI server1 +``` haskell +userAPI :: Proxy UserAPI1 +userAPI = Proxy + +-- 'serve' comes from servant and hands you a WAI Application, +-- which you can think of as an "abstract" web application, +-- not yet a webserver. +app1 :: Application +app1 = serve userAPI server1 +``` The `userAPI` bit is, alas, boilerplate (we need it to guide type inference). But that's about as much boilerplate as you get. And we're done! Let's run our webservice on the port 8081. -> main :: IO () -> main = run 8081 app1 +``` haskell +main :: IO () +main = run 8081 app1 +``` You can put this all into a file or just grab [servant's repo](http://github.com/haskell-servant/servant) and look at the @@ -170,29 +184,35 @@ More endpoints What if we want more than one endpoint? Let's add `/albert` and `/isaac` to view the corresponding users encoded in JSON. -> type UserAPI2 = "users" :> Get '[JSON] [User] -> :<|> "albert" :> Get '[JSON] User -> :<|> "isaac" :> Get '[JSON] User +``` haskell +type UserAPI2 = "users" :> Get '[JSON] [User] + :<|> "albert" :> Get '[JSON] User + :<|> "isaac" :> Get '[JSON] User +``` And let's adapt our code a bit. -> isaac :: User -> isaac = User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1) -> -> albert :: User -> albert = User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1) -> -> users2 :: [User] -> users2 = [isaac, albert] +``` haskell +isaac :: User +isaac = User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1) + +albert :: User +albert = User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1) + +users2 :: [User] +users2 = [isaac, albert] +``` Now, just like we separate the various endpoints in `UserAPI` with `:<|>`, we are going to separate the handlers with `:<|>` too! They must be provided in the same order as the one they appear in in the API type. -> server2 :: Server UserAPI2 -> server2 = return users2 -> :<|> return albert -> :<|> return isaac +``` haskell +server2 :: Server UserAPI2 +server2 = return users2 + :<|> return albert + :<|> return isaac +``` And that's it! You can run this example with `dist/build/tutorial/tutorial 2` and check out the data available @@ -211,70 +231,74 @@ decoding/encoding data from/to JSON. Never. We are going to use the following data types and functions to implement a server for `API`. -> type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position -> :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage -> :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email -> -> data Position = Position -> { x :: Int -> , y :: Int -> } deriving Generic -> -> instance ToJSON Position -> -> newtype HelloMessage = HelloMessage { msg :: String } -> deriving Generic -> -> instance ToJSON HelloMessage -> -> data ClientInfo = ClientInfo -> { clientName :: String -> , clientEmail :: String -> , clientAge :: Int -> , clientInterestedIn :: [String] -> } deriving Generic -> -> instance FromJSON ClientInfo -> instance ToJSON ClientInfo -> -> data Email = Email -> { from :: String -> , to :: String -> , subject :: String -> , body :: String -> } deriving Generic -> -> instance ToJSON Email -> -> emailForClient :: ClientInfo -> Email -> emailForClient c = Email from' to' subject' body' -> -> where from' = "great@company.com" -> to' = clientEmail c -> subject' = "Hey " ++ clientName c ++ ", we miss you!" -> body' = "Hi " ++ clientName c ++ ",\n\n" -> ++ "Since you've recently turned " ++ show (clientAge c) -> ++ ", have you checked out our latest " -> ++ intercalate ", " (clientInterestedIn c) -> ++ " products? Give us a visit!" +``` haskell +type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position + :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage + :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email + +data Position = Position + { x :: Int + , y :: Int + } deriving Generic + +instance ToJSON Position + +newtype HelloMessage = HelloMessage { msg :: String } + deriving Generic + +instance ToJSON HelloMessage + +data ClientInfo = ClientInfo + { clientName :: String + , clientEmail :: String + , clientAge :: Int + , clientInterestedIn :: [String] + } deriving Generic + +instance FromJSON ClientInfo +instance ToJSON ClientInfo + +data Email = Email + { from :: String + , to :: String + , subject :: String + , body :: String + } deriving Generic + +instance ToJSON Email + +emailForClient :: ClientInfo -> Email +emailForClient c = Email from' to' subject' body' + + where from' = "great@company.com" + to' = clientEmail c + subject' = "Hey " ++ clientName c ++ ", we miss you!" + body' = "Hi " ++ clientName c ++ ",\n\n" + ++ "Since you've recently turned " ++ show (clientAge c) + ++ ", have you checked out our latest " + ++ intercalate ", " (clientInterestedIn c) + ++ " products? Give us a visit!" +``` We can implement handlers for the three endpoints: -> server3 :: Server API -> server3 = position -> :<|> hello -> :<|> marketing -> -> where position :: Int -> Int -> EitherT ServantErr IO Position -> position x y = return (Position x y) -> -> hello :: Maybe String -> EitherT ServantErr IO HelloMessage -> hello mname = return . HelloMessage $ case mname of -> Nothing -> "Hello, anonymous coward" -> Just n -> "Hello, " ++ n -> -> marketing :: ClientInfo -> EitherT ServantErr IO Email -> marketing clientinfo = return (emailForClient clientinfo) +``` haskell +server3 :: Server API +server3 = position + :<|> hello + :<|> marketing + + where position :: Int -> Int -> EitherT ServantErr IO Position + position x y = return (Position x y) + + hello :: Maybe String -> EitherT ServantErr IO HelloMessage + hello mname = return . HelloMessage $ case mname of + Nothing -> "Hello, anonymous coward" + Just n -> "Hello, " ++ n + + marketing :: ClientInfo -> EitherT ServantErr IO Email + marketing clientinfo = return (emailForClient clientinfo) +``` Did you see that? The types for your handlers changed to be just what we needed! In particular: @@ -337,29 +361,31 @@ decoded to provides a `FromText` instance, it will Just Work. *servant* provides a decent number of instances, but here are some examples of defining your own. -> -- A typical enumeration -> data Direction -> = Up -> | Down -> | Left -> | Right -> -> instance FromText Direction where -> -- requires {-# LANGUAGE OverloadedStrings #-} -> fromText "up" = Just Up -> fromText "down" = Just Down -> fromText "left" = Just Server.Left -> fromText "right" = Just Server.Right -> fromText _ = Nothing -> -> instance ToText Direction where -> toText Up = "up" -> toText Down = "down" -> toText Server.Left = "left" -> toText Server.Right = "right" -> -> newtype UserId = UserId Int64 -> deriving (FromText, ToText) +``` haskell +-- A typical enumeration +data Direction + = Up + | Down + | Left + | Right + +instance FromText Direction where + -- requires {-# LANGUAGE OverloadedStrings #-} + fromText "up" = Just Up + fromText "down" = Just Down + fromText "left" = Just Server.Left + fromText "right" = Just Server.Right + fromText _ = Nothing + +instance ToText Direction where + toText Up = "up" + toText Down = "down" + toText Server.Left = "left" + toText Server.Right = "right" + +newtype UserId = UserId Int64 + deriving (FromText, ToText) +``` or writing the instances by hand: @@ -464,10 +490,12 @@ our own little function around *aeson* and *attoparsec* that allows any type of JSON value at the toplevel of a "JSON document". Here's the definition in case you are curious. -> eitherDecodeLenient :: FromJSON a => ByteString -> Either String a -> eitherDecodeLenient input = do -> v :: Value <- parseOnly (Data.Aeson.Parser.value <* endOfInput) (cs input) -> parseEither parseJSON v +``` haskell +eitherDecodeLenient :: FromJSON a => ByteString -> Either String a +eitherDecodeLenient input = do + v :: Value <- parseOnly (Data.Aeson.Parser.value <* endOfInput) (cs input) + parseEither parseJSON v +``` This function is exactly what we need for our `MimeUnrender` instance. @@ -492,7 +520,9 @@ or [lucid](http://hackage.haskell.org/package/lucid). The best option for *servant* is obviously to support both (and hopefully other templating solutions!). -> data HTMLLucid +``` haskell +data HTMLLucid +``` Once again, the data type is just there as a symbol for the encoding/decoding functions, except that this time we will only worry about encoding since @@ -500,8 +530,10 @@ functions, except that this time we will only worry about encoding since Both packages also have the same `Accept` instance for their `HTMLLucid` type. -> instance Accept HTMLLucid where -> contentType _ = "text" // "html" /: ("charset", "utf-8") +``` haskell +instance Accept HTMLLucid where + contentType _ = "text" // "html" /: ("charset", "utf-8") +``` Note that this instance uses the `(/:)` operator from *http-media* which lets us specify additional information about a content-type, like the charset here. @@ -512,31 +544,35 @@ then write that to a `ByteString`. For *lucid*: -> instance ToHtml a => MimeRender HTMLLucid a where -> mimeRender _ = renderBS . toHtml -> -> -- let's also provide an instance for lucid's -> -- 'Html' wrapper. -> instance MimeRender HTMLLucid (Html a) where -> mimeRender _ = renderBS +``` haskell +instance ToHtml a => MimeRender HTMLLucid a where + mimeRender _ = renderBS . toHtml + +-- let's also provide an instance for lucid's +-- 'Html' wrapper. +instance MimeRender HTMLLucid (Html a) where + mimeRender _ = renderBS +``` For *blaze-html*: -> -- For this tutorial to compile 'HTMLLucid' and 'HTMLBlaze' have to be -> -- distinct. Usually you would stick to one html rendering library and then -> -- you can go with one 'HTML' type. -> data HTMLBlaze -> -> instance Accept HTMLBlaze where -> contentType _ = "text" // "html" /: ("charset", "utf-8") -> -> instance ToMarkup a => MimeRender HTMLBlaze a where -> mimeRender _ = renderHtml . Text.Blaze.Html.toHtml -> -> -- while we're at it, just like for lucid we can -> -- provide an instance for rendering blaze's 'Html' type -> instance MimeRender HTMLBlaze Text.Blaze.Html.Html where -> mimeRender _ = renderHtml +``` haskell +-- For this tutorial to compile 'HTMLLucid' and 'HTMLBlaze' have to be +-- distinct. Usually you would stick to one html rendering library and then +-- you can go with one 'HTML' type. +data HTMLBlaze + +instance Accept HTMLBlaze where + contentType _ = "text" // "html" /: ("charset", "utf-8") + +instance ToMarkup a => MimeRender HTMLBlaze a where + mimeRender _ = renderHtml . Text.Blaze.Html.toHtml + +-- while we're at it, just like for lucid we can +-- provide an instance for rendering blaze's 'Html' type +instance MimeRender HTMLBlaze Text.Blaze.Html.Html where + mimeRender _ = renderHtml +``` Both [servant-blaze](http://hackage.haskell.org/package/servant-blaze) and [servant-lucid](http://hackage.haskell.org/package/servant-lucid) let you use @@ -548,59 +584,67 @@ content type in action. First off, imports and pragmas as usual. We will be serving the following API: -> type PersonAPI = "persons" :> Get '[JSON, HTMLLucid] [Person] +``` haskell +type PersonAPI = "persons" :> Get '[JSON, HTMLLucid] [Person] +``` where `Person` is defined as follows: -> data Person = Person -> { firstName :: String -> , lastName :: String -> } deriving Generic -- for the JSON instance -> -> instance ToJSON Person +``` haskell +data Person = Person + { firstName :: String + , lastName :: String + } deriving Generic -- for the JSON instance + +instance ToJSON Person +``` Now, let's teach *lucid* how to render a `Person` as a row in a table, and then a list of `Person`s as a table with a row per person. -> -- HTML serialization of a single person -> instance ToHtml Person where -> toHtml person = -> tr_ $ do -> td_ (toHtml $ firstName person) -> td_ (toHtml $ lastName person) -> -> -- do not worry too much about this -> toHtmlRaw = toHtml -> -> -- HTML serialization of a list of persons -> instance ToHtml [Person] where -> toHtml persons = table_ $ do -> tr_ $ do -> th_ "first name" -> th_ "last name" -> -> -- this just calls toHtml on each person of the list -> -- and concatenates the resulting pieces of HTML together -> foldMap toHtml persons -> -> toHtmlRaw = toHtml +``` haskell +-- HTML serialization of a single person +instance ToHtml Person where + toHtml person = + tr_ $ do + td_ (toHtml $ firstName person) + td_ (toHtml $ lastName person) + + -- do not worry too much about this + toHtmlRaw = toHtml + +-- HTML serialization of a list of persons +instance ToHtml [Person] where + toHtml persons = table_ $ do + tr_ $ do + th_ "first name" + th_ "last name" + + -- this just calls toHtml on each person of the list + -- and concatenates the resulting pieces of HTML together + foldMap toHtml persons + + toHtmlRaw = toHtml +``` We create some `Person` values and serve them as a list: -> persons :: [Person] -> persons = -> [ Person "Isaac" "Newton" -> , Person "Albert" "Einstein" -> ] -> -> personAPI :: Proxy PersonAPI -> personAPI = Proxy -> -> server4 :: Server PersonAPI -> server4 = return persons -> -> app2 :: Application -> app2 = serve personAPI server4 +``` haskell +persons :: [Person] +persons = + [ Person "Isaac" "Newton" + , Person "Albert" "Einstein" + ] + +personAPI :: Proxy PersonAPI +personAPI = Proxy + +server4 :: Server PersonAPI +server4 = return persons + +app2 :: Application +app2 = serve personAPI server4 +``` And we're good to go. You can run this example with `dist/build/tutorial/tutorial 4`. @@ -666,18 +710,20 @@ class Monad m => MonadIO m where Obviously, the `IO` monad provides a `MonadIO` instance. Hence for any type `e`, `EitherT e IO` has a `MonadIO` instance. So if you want to run any kind of IO computation in your handlers, just use `liftIO`: -> type IOAPI1 = "myfile.txt" :> Get '[JSON] FileContent -> -> newtype FileContent = FileContent -> { content :: String } -> deriving Generic -> -> instance ToJSON FileContent -> -> server5 :: Server IOAPI1 -> server5 = do -> filecontent <- liftIO (readFile "myfile.txt") -> return (FileContent filecontent) +``` haskell +type IOAPI1 = "myfile.txt" :> Get '[JSON] FileContent + +newtype FileContent = FileContent + { content :: String } + deriving Generic + +instance ToJSON FileContent + +server5 :: Server IOAPI1 +server5 = do + filecontent <- liftIO (readFile "myfile.txt") + return (FileContent filecontent) +``` Failing, through `ServantErr` ----------------------------- @@ -701,23 +747,27 @@ Many standard values are provided out of the box by the `Servant.Server` module. If you want to use these values but add a body or some headers, just use record update syntax: -> failingHandler :: EitherT ServantErr IO () -> failingHandler = left myerr -> -> where myerr :: ServantErr -> myerr = err503 { errBody = "Sorry dear user." } +``` haskell +failingHandler :: EitherT ServantErr IO () +failingHandler = left myerr + + where myerr :: ServantErr + myerr = err503 { errBody = "Sorry dear user." } +``` Here's an example where we return a customised 404-Not-Found error message in the response body if "myfile.txt" isn't there: -> server6 :: Server IOAPI1 -> server6 = do -> exists <- liftIO (doesFileExist "myfile.txt") -> if exists -> then liftIO (readFile "myfile.txt") >>= return . FileContent -> else left custom404Err -> -> where custom404Err = err404 { errBody = "myfile.txt just isn't there, please leave this server alone." } +``` haskell +server6 :: Server IOAPI1 +server6 = do + exists <- liftIO (doesFileExist "myfile.txt") + if exists + then liftIO (readFile "myfile.txt") >>= return . FileContent + else left custom404Err + + where custom404Err = err404 { errBody = "myfile.txt just isn't there, please leave this server alone." } +``` Let's run this server (`dist/build/tutorial/tutorial 5`) and query it, first without the file and then with the file. @@ -758,10 +808,12 @@ Response headers To add headers to your response, use [addHeader](http://hackage.haskell.org/package/servant-0.4.4/docs/Servant-API-ResponseHeaders.html). Note that this changes the type of your API, as we can see in the following example: -> type MyHandler = Get '[JSON] (Headers '[Header "X-An-Int" Int] User) -> -> myHandler :: Server MyHandler -> myHandler = return $ addHeader 1797 albert +``` haskell +type MyHandler = Get '[JSON] (Headers '[Header "X-An-Int" Int] User) + +myHandler :: Server MyHandler +myHandler = return $ addHeader 1797 albert +``` Serving static files @@ -786,18 +838,24 @@ getting-started. The API type will be the following. -> type CodeAPI = "code" :> Raw +``` haskell +type CodeAPI = "code" :> Raw +``` And the server: -> codeAPI :: Proxy CodeAPI -> codeAPI = Proxy +``` haskell +codeAPI :: Proxy CodeAPI +codeAPI = Proxy +``` -> server7 :: Server CodeAPI -> server7 = serveDirectory "tutorial" -> -> app3 :: Application -> app3 = serve codeAPI server7 +``` haskell +server7 :: Server CodeAPI +server7 = serveDirectory "tutorial" + +app3 :: Application +app3 = serve codeAPI server7 +``` This server will match any request whose path starts with `/code` and will look for a file at the path described by the rest of the request path, inside the *tutorial/* directory of the path you run the program from. @@ -903,18 +961,22 @@ Nested APIs Let's see how you can define APIs in a modular way, while avoiding repetition. Consider this simple example: -> type UserAPI3 = -- view the user with given userid, in JSON -> Capture "userid" Int :> Get '[JSON] User -> -> :<|> -- delete the user with given userid. empty response -> Capture "userid" Int :> Delete '[] () +``` haskell +type UserAPI3 = -- view the user with given userid, in JSON + Capture "userid" Int :> Get '[JSON] User + + :<|> -- delete the user with given userid. empty response + Capture "userid" Int :> Delete '[] () +``` We can instead factor out the `userid`: -> type UserAPI4 = Capture "userid" Int :> -> ( Get '[JSON] User -> :<|> Delete '[] () -> ) +``` haskell +type UserAPI4 = Capture "userid" Int :> + ( Get '[JSON] User + :<|> Delete '[] () + ) +``` However, you have to be aware that this has an effect on the type of the corresponding `Server`: @@ -930,146 +992,158 @@ Server UserAPI4 = Int -> ( EitherT ServantErr IO User In the first case, each handler receives the *userid* argument. In the latter, the whole `Server` takes the *userid* and has handlers that are just computations in `EitherT`, with no arguments. In other words: -> server8 :: Server UserAPI3 -> server8 = getUser :<|> deleteUser -> -> where getUser :: Int -> EitherT ServantErr IO User -> getUser _userid = error "..." -> -> deleteUser :: Int -> EitherT ServantErr IO () -> deleteUser _userid = error "..." -> -> -- notice how getUser and deleteUser -> -- have a different type! no argument anymore, -> -- the argument directly goes to the whole Server -> server9 :: Server UserAPI4 -> server9 userid = getUser userid :<|> deleteUser userid -> -> where getUser :: Int -> EitherT ServantErr IO User -> getUser = error "..." -> -> deleteUser :: Int -> EitherT ServantErr IO () -> deleteUser = error "..." +``` haskell +server8 :: Server UserAPI3 +server8 = getUser :<|> deleteUser + + where getUser :: Int -> EitherT ServantErr IO User + getUser _userid = error "..." + + deleteUser :: Int -> EitherT ServantErr IO () + deleteUser _userid = error "..." + +-- notice how getUser and deleteUser +-- have a different type! no argument anymore, +-- the argument directly goes to the whole Server +server9 :: Server UserAPI4 +server9 userid = getUser userid :<|> deleteUser userid + + where getUser :: Int -> EitherT ServantErr IO User + getUser = error "..." + + deleteUser :: Int -> EitherT ServantErr IO () + deleteUser = error "..." +``` Note that there's nothing special about `Capture` that lets you "factor it out": this can be done with any combinator. Here are a few examples of APIs with a combinator factored out for which we can write a perfectly valid `Server`. -> -- we just factor out the "users" path fragment -> type API1 = "users" :> -> ( Get '[JSON] [User] -- user listing -> :<|> Capture "userid" Int :> Get '[JSON] User -- view a particular user -> ) -> -> -- we factor out the Request Body -> type API2 = ReqBody '[JSON] User :> -> ( Get '[JSON] User -- just display the same user back, don't register it -> :<|> Post '[JSON] () -- register the user. empty response -> ) -> -> -- we factor out a Header -> type API3 = Header "Authorization" Token :> -> ( Get '[JSON] SecretData -- get some secret data, if authorized -> :<|> ReqBody '[JSON] SecretData :> Post '[] () -- add some secret data, if authorized -> ) -> -> newtype Token = Token ByteString -> newtype SecretData = SecretData ByteString +``` haskell +-- we just factor out the "users" path fragment +type API1 = "users" :> + ( Get '[JSON] [User] -- user listing + :<|> Capture "userid" Int :> Get '[JSON] User -- view a particular user + ) + +-- we factor out the Request Body +type API2 = ReqBody '[JSON] User :> + ( Get '[JSON] User -- just display the same user back, don't register it + :<|> Post '[JSON] () -- register the user. empty response + ) + +-- we factor out a Header +type API3 = Header "Authorization" Token :> + ( Get '[JSON] SecretData -- get some secret data, if authorized + :<|> ReqBody '[JSON] SecretData :> Post '[] () -- add some secret data, if authorized + ) + +newtype Token = Token ByteString +newtype SecretData = SecretData ByteString +``` This approach lets you define APIs modularly and assemble them all into one big API type only at the end. -> type UsersAPI = -> Get '[JSON] [User] -- list users -> :<|> ReqBody '[JSON] User :> Post '[] () -- add a user -> :<|> Capture "userid" Int :> -> ( Get '[JSON] User -- view a user -> :<|> ReqBody '[JSON] User :> Put '[] () -- update a user -> :<|> Delete '[] () -- delete a user -> ) -> -> usersServer :: Server UsersAPI -> usersServer = getUsers :<|> newUser :<|> userOperations -> -> where getUsers :: EitherT ServantErr IO [User] -> getUsers = error "..." -> -> newUser :: User -> EitherT ServantErr IO () -> newUser = error "..." -> -> userOperations userid = -> viewUser userid :<|> updateUser userid :<|> deleteUser userid -> -> where -> viewUser :: Int -> EitherT ServantErr IO User -> viewUser = error "..." -> -> updateUser :: Int -> User -> EitherT ServantErr IO () -> updateUser = error "..." -> -> deleteUser :: Int -> EitherT ServantErr IO () -> deleteUser = error "..." +``` haskell +type UsersAPI = + Get '[JSON] [User] -- list users + :<|> ReqBody '[JSON] User :> Post '[] () -- add a user + :<|> Capture "userid" Int :> + ( Get '[JSON] User -- view a user + :<|> ReqBody '[JSON] User :> Put '[] () -- update a user + :<|> Delete '[] () -- delete a user + ) -> type ProductsAPI = -> Get '[JSON] [Product] -- list products -> :<|> ReqBody '[JSON] Product :> Post '[] () -- add a product -> :<|> Capture "productid" Int :> -> ( Get '[JSON] Product -- view a product -> :<|> ReqBody '[JSON] Product :> Put '[] () -- update a product -> :<|> Delete '[] () -- delete a product -> ) -> -> data Product = Product { productId :: Int } -> -> productsServer :: Server ProductsAPI -> productsServer = getProducts :<|> newProduct :<|> productOperations -> -> where getProducts :: EitherT ServantErr IO [Product] -> getProducts = error "..." -> -> newProduct :: Product -> EitherT ServantErr IO () -> newProduct = error "..." -> -> productOperations productid = -> viewProduct productid :<|> updateProduct productid :<|> deleteProduct productid -> -> where -> viewProduct :: Int -> EitherT ServantErr IO Product -> viewProduct = error "..." -> -> updateProduct :: Int -> Product -> EitherT ServantErr IO () -> updateProduct = error "..." -> -> deleteProduct :: Int -> EitherT ServantErr IO () -> deleteProduct = error "..." +usersServer :: Server UsersAPI +usersServer = getUsers :<|> newUser :<|> userOperations -> type CombinedAPI = "users" :> UsersAPI -> :<|> "products" :> ProductsAPI -> -> server10 :: Server CombinedAPI -> server10 = usersServer :<|> productsServer + where getUsers :: EitherT ServantErr IO [User] + getUsers = error "..." + + newUser :: User -> EitherT ServantErr IO () + newUser = error "..." + + userOperations userid = + viewUser userid :<|> updateUser userid :<|> deleteUser userid + + where + viewUser :: Int -> EitherT ServantErr IO User + viewUser = error "..." + + updateUser :: Int -> User -> EitherT ServantErr IO () + updateUser = error "..." + + deleteUser :: Int -> EitherT ServantErr IO () + deleteUser = error "..." +``` + +``` haskell +type ProductsAPI = + Get '[JSON] [Product] -- list products + :<|> ReqBody '[JSON] Product :> Post '[] () -- add a product + :<|> Capture "productid" Int :> + ( Get '[JSON] Product -- view a product + :<|> ReqBody '[JSON] Product :> Put '[] () -- update a product + :<|> Delete '[] () -- delete a product + ) + +data Product = Product { productId :: Int } + +productsServer :: Server ProductsAPI +productsServer = getProducts :<|> newProduct :<|> productOperations + + where getProducts :: EitherT ServantErr IO [Product] + getProducts = error "..." + + newProduct :: Product -> EitherT ServantErr IO () + newProduct = error "..." + + productOperations productid = + viewProduct productid :<|> updateProduct productid :<|> deleteProduct productid + + where + viewProduct :: Int -> EitherT ServantErr IO Product + viewProduct = error "..." + + updateProduct :: Int -> Product -> EitherT ServantErr IO () + updateProduct = error "..." + + deleteProduct :: Int -> EitherT ServantErr IO () + deleteProduct = error "..." +``` + +``` haskell +type CombinedAPI = "users" :> UsersAPI + :<|> "products" :> ProductsAPI + +server10 :: Server CombinedAPI +server10 = usersServer :<|> productsServer +``` Finally, we can realize the user and product APIs are quite similar and abstract that away: -> -- API for values of type 'a' -> -- indexed by values of type 'i' -> type APIFor a i = -> Get '[JSON] [a] -- list 'a's -> :<|> ReqBody '[JSON] a :> Post '[] () -- add an 'a' -> :<|> Capture "id" i :> -> ( Get '[JSON] a -- view an 'a' given its "identifier" of type 'i' -> :<|> ReqBody '[JSON] a :> Put '[] () -- update an 'a' -> :<|> Delete '[] () -- delete an 'a' -> ) -> -> -- Build the appropriate 'Server' -> -- given the handlers of the right type. -> serverFor :: EitherT ServantErr IO [a] -- handler for listing of 'a's -> -> (a -> EitherT ServantErr IO ()) -- handler for adding an 'a' -> -> (i -> EitherT ServantErr IO a) -- handler for viewing an 'a' given its identifier of type 'i' -> -> (i -> a -> EitherT ServantErr IO ()) -- updating an 'a' with given id -> -> (i -> EitherT ServantErr IO ()) -- deleting an 'a' given its id -> -> Server (APIFor a i) -> serverFor = error "..." -> -- implementation left as an exercise. contact us on IRC -> -- or the mailing list if you get stuck! +``` haskell +-- API for values of type 'a' +-- indexed by values of type 'i' +type APIFor a i = + Get '[JSON] [a] -- list 'a's + :<|> ReqBody '[JSON] a :> Post '[] () -- add an 'a' + :<|> Capture "id" i :> + ( Get '[JSON] a -- view an 'a' given its "identifier" of type 'i' + :<|> ReqBody '[JSON] a :> Put '[] () -- update an 'a' + :<|> Delete '[] () -- delete an 'a' + ) + +-- Build the appropriate 'Server' +-- given the handlers of the right type. +serverFor :: EitherT ServantErr IO [a] -- handler for listing of 'a's + -> (a -> EitherT ServantErr IO ()) -- handler for adding an 'a' + -> (i -> EitherT ServantErr IO a) -- handler for viewing an 'a' given its identifier of type 'i' + -> (i -> a -> EitherT ServantErr IO ()) -- updating an 'a' with given id + -> (i -> EitherT ServantErr IO ()) -- deleting an 'a' given its id + -> Server (APIFor a i) +serverFor = error "..." +-- implementation left as an exercise. contact us on IRC +-- or the mailing list if you get stuck! +``` Using another monad for your handlers ===================================== @@ -1112,28 +1186,32 @@ computation by supplying it with a `String`, like `"hi"`. We get an `a` out from that and can then just `return` it into `EitherT`. We can then just wrap that function with the `Nat` constructor to make it have the fancier type. -> readerToEither' :: forall a. Reader String a -> EitherT ServantErr IO a -> readerToEither' r = return (runReader r "hi") -> -> readerToEither :: Reader String :~> EitherT ServantErr IO -> readerToEither = Nat readerToEither' +``` haskell +readerToEither' :: forall a. Reader String a -> EitherT ServantErr IO a +readerToEither' r = return (runReader r "hi") + +readerToEither :: Reader String :~> EitherT ServantErr IO +readerToEither = Nat readerToEither' +``` We can write some simple webservice with the handlers running in `Reader String`. -> type ReaderAPI = "a" :> Get '[JSON] Int -> :<|> "b" :> Get '[JSON] String -> -> readerAPI :: Proxy ReaderAPI -> readerAPI = Proxy -> -> readerServerT :: ServerT ReaderAPI (Reader String) -> readerServerT = a :<|> b -> -> where a :: Reader String Int -> a = return 1797 -> -> b :: Reader String String -> b = ask +``` haskell +type ReaderAPI = "a" :> Get '[JSON] Int + :<|> "b" :> Get '[JSON] String + +readerAPI :: Proxy ReaderAPI +readerAPI = Proxy + +readerServerT :: ServerT ReaderAPI (Reader String) +readerServerT = a :<|> b + + where a :: Reader String Int + a = return 1797 + + b :: Reader String String + b = ask +``` We unfortunately can't use `readerServerT` as an argument of `serve`, because `serve` wants a `Server ReaderAPI`, i.e., with handlers running in `EitherT @@ -1150,11 +1228,13 @@ and `n` and a `ServerT someapi m`, and returns a `ServerT someapi n`. In our case, we can wrap up our little webservice by using `enter readerToEither` on our handlers. -> readerServer :: Server ReaderAPI -> readerServer = enter readerToEither readerServerT -> -> app4 :: Application -> app4 = serve readerAPI readerServer +``` haskell +readerServer :: Server ReaderAPI +readerServer = enter readerToEither readerServerT + +app4 :: Application +app4 = serve readerAPI readerServer +``` And we can indeed see this webservice in action by running `dist/build/tutorial/tutorial 7`. From a7c3880c67a4a099708b53a03424ec18e978960f Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Wed, 27 Jan 2016 22:30:18 +0100 Subject: [PATCH 11/50] Remove convert script --- doc/tutorial/convert.hs | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 doc/tutorial/convert.hs diff --git a/doc/tutorial/convert.hs b/doc/tutorial/convert.hs deleted file mode 100644 index ffcb60a7..00000000 --- a/doc/tutorial/convert.hs +++ /dev/null @@ -1,30 +0,0 @@ - -import Control.Arrow -import Data.Foldable -import Data.List -import System.Environment - -main = do - files <- getArgs - forM_ files $ \ file -> do - convertM file - -convertM :: FilePath -> IO () -convertM file = do - contents <- readFile file - seq (length contents) (return ()) - writeFile file (convert contents) - -convert :: String -> String -convert = - lines >>> - groupBy (\ a b -> take 1 a == take 1 b) >>> - map go >>> - concat >>> - unlines - where - go :: [String] -> [String] - go (a : r) - | ">" `isPrefixOf` a - = "``` haskell ignore" : map (drop 2) (a : r) ++ "```" : [] - go x = x From f601cbf3b12cf4381f0fa0e3e69cff4ec4259444 Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Wed, 27 Jan 2016 22:46:28 +0100 Subject: [PATCH 12/50] Fix cabal file, stack.yaml, and sources.txt --- doc/tutorial/{api-type.lhs => ApiType.lhs} | 72 ++++++++----------- doc/tutorial/{client.lhs => Client.lhs} | 0 doc/tutorial/{docs.lhs => Docs.lhs} | 0 .../{javascript.lhs => Javascript.lhs} | 0 doc/tutorial/{server.lhs => Server.lhs} | 0 doc/tutorial/tutorial.cabal | 11 ++- sources.txt | 1 - stack.yaml | 2 +- 8 files changed, 41 insertions(+), 45 deletions(-) rename doc/tutorial/{api-type.lhs => ApiType.lhs} (86%) rename doc/tutorial/{client.lhs => Client.lhs} (100%) rename doc/tutorial/{docs.lhs => Docs.lhs} (100%) rename doc/tutorial/{javascript.lhs => Javascript.lhs} (100%) rename doc/tutorial/{server.lhs => Server.lhs} (100%) diff --git a/doc/tutorial/api-type.lhs b/doc/tutorial/ApiType.lhs similarity index 86% rename from doc/tutorial/api-type.lhs rename to doc/tutorial/ApiType.lhs index fbf42644..b13f1747 100644 --- a/doc/tutorial/api-type.lhs +++ b/doc/tutorial/ApiType.lhs @@ -1,7 +1,4 @@ ---- -title: A web API as a type -toc: true ---- +# A web API as a type The source for this tutorial section is a literate haskell file, so first we need to have some language extensions and imports: @@ -25,8 +22,7 @@ Consider the following informal specification of an API: You *should* be able to formalize that. And then use the formalized version to get you much of the way towards writing a web app. And all the way towards -getting some client libraries, and documentation (and in the future, who knows -- tests, HATEOAS, ...). +getting some client libraries, and documentation, and more. How would we describe it with servant? As mentioned earlier, an endpoint description is a good old Haskell **type**: @@ -45,22 +41,22 @@ data User = User { Let's break that down: - `"users"` says that our endpoint will be accessible under `/users`; -- `QueryParam "sortby" SortBy`, where `SortBy` is defined by `data SortBy = Age -| Name`, says that the endpoint has a query string parameter named `sortby` -whose value will be extracted as a value of type `SortBy`. +- `QueryParam "sortby" SortBy`, where `SortBy` is defined by `data SortBy = Age | Name`, + says that the endpoint has a query string parameter named `sortby` + whose value will be extracted as a value of type `SortBy`. - `Get '[JSON] [User]` says that the endpoint will be accessible through HTTP -GET requests, returning a list of users encoded as JSON. You will see -later how you can make use of this to make your data available under different -formats, the choice being made depending on the [Accept -header](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html) specified in -the client's request. -- the `:>` operator that separates the various "combinators" just lets you -sequence static path fragments, URL captures and other combinators. The -ordering only matters for static path fragments and URL captures. `"users" :> -"list-all" :> Get '[JSON] [User]`, equivalent to `/users/list-all`, is -obviously not the same as `"list-all" :> "users" :> Get '[JSON] [User]`, which -is equivalent to `/list-all/users`. This means that sometimes `:>` is somehow -equivalent to `/`, but sometimes it just lets you chain another combinator. + GET requests, returning a list of users encoded as JSON. You will see + later how you can make use of this to make your data available under different + formats, the choice being made depending on the [Accept + header](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html) specified in + the client's request. +- The `:>` operator that separates the various "combinators" just lets you + sequence static path fragments, URL captures and other combinators. The + ordering only matters for static path fragments and URL captures. `"users" :> + "list-all" :> Get '[JSON] [User]`, equivalent to `/users/list-all`, is + obviously not the same as `"list-all" :> "users" :> Get '[JSON] [User]`, which + is equivalent to `/list-all/users`. This means that sometimes `:>` is somehow + equivalent to `/`, but sometimes it just lets you chain another combinator. We can also describe APIs with multiple endpoints by using the `:<|>` combinators. Here's an example: @@ -74,11 +70,10 @@ type UserAPI2 = "users" :> "list-all" :> Get '[JSON] [User] always write your own when you need it. Here's a quick overview of all the combinators that servant comes with. -Combinators -=========== +## Combinators + +### Static strings -Static strings --------------- As you've already seen, you can use type-level strings (enabled with the `DataKinds` language extension) for static path fragments. Chaining @@ -90,8 +85,8 @@ type UserAPI3 = "users" :> "list-all" :> "now" :> Get '[JSON] [User] -- /users/list-all/now ``` -`Delete`, `Get`, `Patch`, `Post` and `Put` ------------------------------------------- +### `Delete`, `Get`, `Patch`, `Post` and `Put` + These 5 combinators are very similar except that they each describe a different HTTP method. This is how they're declared @@ -112,8 +107,8 @@ type UserAPI4 = "users" :> Get '[JSON] [User] :<|> "admins" :> Get '[JSON] [User] ``` -`Capture` ---------- +### `Capture` + URL captures are parts of the URL that are variable and whose actual value is captured and passed to the request handlers. In many web frameworks, you'll see @@ -147,8 +142,7 @@ type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User -- equivalent to 'DELETE /user/:userid' ``` -`QueryParam`, `QueryParams`, `QueryFlag`, `MatrixParam`, `MatrixParams` and `MatrixFlag` ----------------------------------------------------------------------------------------- +### `QueryParam`, `QueryParams`, `QueryFlag`, `MatrixParam`, `MatrixParams` and `MatrixFlag` `QueryParam`, `QueryParams` and `QueryFlag` are about query string parameters, i.e., those parameters that come after the question mark @@ -202,8 +196,7 @@ type UserAPI6 = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User] Again, your handlers don't have to deserialize these things (into, for example, a `SortBy`). *servant* takes care of it. -`ReqBody` ---------- +### `ReqBody` Each HTTP request can carry some additional data that the server can use in its *body*, and this data can be encoded in any format -- as long as the server @@ -240,8 +233,8 @@ type UserAPI7 = "users" :> ReqBody '[JSON] User :> Post '[JSON] User -- - returns a User encoded in JSON ``` -Request `Header`s ------------------ +### Request `Header`s + Request headers are used for various purposes, from caching to carrying auth-related data. They consist of a header name and an associated value. An @@ -263,8 +256,7 @@ the client to send the request. type UserAPI8 = "users" :> Header "User-Agent" Text :> Get '[JSON] [User] ``` -Content types -------------- +### Content types So far, whenever we have used a combinator that carries a list of content types, we've always specified `'[JSON]`. However, *servant* lets you use several @@ -286,8 +278,7 @@ that everyone uses, we decided to release 2 packages, *servant-lucid* and We will further explain how these content types and your data types can play together in the [section about serving an API](/tutorial/server.html). -Response `Headers` ------------------- +### Response `Headers` Just like an HTTP request, the response generated by a webserver can carry headers too. *servant* provides a `Headers` combinator that carries a list of @@ -305,8 +296,7 @@ response, you could write it as below: type UserAPI10 = "users" :> Get '[JSON] (Headers '[Header "User-Count" Integer] [User]) ``` -Interoperability with other WAI `Application`s: `Raw` ------------------------------------------------------ +### Interoperability with other WAI `Application`s: `Raw` Finally, we also include a combinator named `Raw` that can be used for two reasons: diff --git a/doc/tutorial/client.lhs b/doc/tutorial/Client.lhs similarity index 100% rename from doc/tutorial/client.lhs rename to doc/tutorial/Client.lhs diff --git a/doc/tutorial/docs.lhs b/doc/tutorial/Docs.lhs similarity index 100% rename from doc/tutorial/docs.lhs rename to doc/tutorial/Docs.lhs diff --git a/doc/tutorial/javascript.lhs b/doc/tutorial/Javascript.lhs similarity index 100% rename from doc/tutorial/javascript.lhs rename to doc/tutorial/Javascript.lhs diff --git a/doc/tutorial/server.lhs b/doc/tutorial/Server.lhs similarity index 100% rename from doc/tutorial/server.lhs rename to doc/tutorial/Server.lhs diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index 47fc2ebd..37e60d39 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -1,5 +1,5 @@ name: tutorial -version: 0.1.0.0 +version: 0.5 synopsis: The servant tutorial -- description: homepage: http://haskell-servant.github.io/ @@ -14,9 +14,16 @@ build-type: Simple cabal-version: >=1.10 library - exposed-modules: api-type.lhs + exposed-modules: ApiType + , Client + , Docs + , Javascript + , Server -- other-modules: -- other-extensions: build-depends: base >=4.8 && <4.9 + , text + , servant -- hs-source-dirs: default-language: Haskell2010 + ghc-options: -Wall -Werror -c -pgmL markdown-unlit diff --git a/sources.txt b/sources.txt index 24719355..2d3f8107 100644 --- a/sources.txt +++ b/sources.txt @@ -5,7 +5,6 @@ servant-docs servant-foreign servant-js servant-server -servant-examples servant-blaze servant-lucid servant-mock diff --git a/stack.yaml b/stack.yaml index adec1495..feaea42b 100644 --- a/stack.yaml +++ b/stack.yaml @@ -7,12 +7,12 @@ packages: - servant-cassava/ - servant-client/ - servant-docs/ -- servant-examples/ - servant-foreign/ - servant-js/ - servant-lucid/ - servant-mock/ - servant-server/ +- doc/tutorial extra-deps: - base-compat-0.9.0 - engine-io-wai-1.0.2 From c6dfac5203daf7b4ca0359633671ee5689070ee9 Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Wed, 27 Jan 2016 22:49:09 +0100 Subject: [PATCH 13/50] Remove matrix params --- doc/tutorial/ApiType.lhs | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/doc/tutorial/ApiType.lhs b/doc/tutorial/ApiType.lhs index b13f1747..28ecb5e8 100644 --- a/doc/tutorial/ApiType.lhs +++ b/doc/tutorial/ApiType.lhs @@ -142,7 +142,7 @@ type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User -- equivalent to 'DELETE /user/:userid' ``` -### `QueryParam`, `QueryParams`, `QueryFlag`, `MatrixParam`, `MatrixParams` and `MatrixFlag` +### `QueryParam`, `QueryParams`, `QueryFlag` `QueryParam`, `QueryParams` and `QueryFlag` are about query string parameters, i.e., those parameters that come after the question mark @@ -165,32 +165,12 @@ data QueryParams (sym :: Symbol) a data QueryFlag (sym :: Symbol) ``` -[Matrix parameters](http://www.w3.org/DesignIssues/MatrixURIs.html) -are similar to query string parameters, but they can appear anywhere -in the paths (click the link for more details). A URL with matrix -parameters in it looks like `/users;sortby=age`, as opposed to -`/users?sortby=age` with query string parameters. The big advantage is -that they are not necessarily at the end of the URL. You could have -`/users;active=true;registered_after=2005-01-01/locations` to get -geolocation data about users whom are still active and registered -after *January 1st, 2005*. - -Corresponding data type declarations below. - -``` haskell ignore -data MatrixParam (sym :: Symbol) a -data MatrixParams (sym :: Symbol) a -data MatrixFlag (sym :: Symbol) -``` - Examples: ``` haskell type UserAPI6 = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User] -- equivalent to 'GET /users?sortby={age, name}' - :<|> "users" :> MatrixParam "sortby" SortBy :> Get '[JSON] [User] - -- equivalent to 'GET /users;sortby={age, name}' ``` Again, your handlers don't have to deserialize these things (into, for example, From 8e63078691c1ae4f4c86d89397fe0e594cee553b Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Wed, 27 Jan 2016 22:54:22 +0100 Subject: [PATCH 14/50] compiling ApiType.lhs --- doc/tutorial/tutorial.cabal | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index 37e60d39..5a907a65 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -15,15 +15,35 @@ cabal-version: >=1.10 library exposed-modules: ApiType - , Client - , Docs - , Javascript - , Server + -- , Client + -- , Docs + -- , Javascript + -- , Server -- other-modules: -- other-extensions: build-depends: base >=4.8 && <4.9 , text - , servant + , aeson + , blaze-html + , directory + , blaze-markup + , servant == 0.5.* + , servant-server == 0.5.* + , servant-client == 0.5.* + , servant-docs == 0.5.* + , warp + , http-media + , lucid + , time + , string-conversions + , bytestring + , attoparsec + , mtl + , random + , js-jquery + , wai + , http-types + , transformers -- hs-source-dirs: default-language: Haskell2010 - ghc-options: -Wall -Werror -c -pgmL markdown-unlit + ghc-options: -Wall -Werror -pgmL markdown-unlit From 8990ebb16e89baaa93f8f0f92fc3add52641e788 Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Wed, 27 Jan 2016 22:58:38 +0100 Subject: [PATCH 15/50] Fix toctree and page titles --- doc/tutorial/Client.lhs | 5 +---- doc/tutorial/Docs.lhs | 5 +---- doc/tutorial/Javascript.lhs | 5 +---- doc/tutorial/index.rst | 10 +++++----- 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/doc/tutorial/Client.lhs b/doc/tutorial/Client.lhs index 9571ec8c..1a186596 100644 --- a/doc/tutorial/Client.lhs +++ b/doc/tutorial/Client.lhs @@ -1,7 +1,4 @@ ---- -title: Deriving Haskell functions to query an API -toc: true ---- +# Deriving Haskell functions to query an API While defining handlers that serve an API has a lot to it, querying an API is simpler: we do not care about what happens inside the webserver, we just need to know how to talk to it and get a response back. Except that we usually have to write the querying functions by hand because the structure of the API isn't a first class citizen and can't be inspected to generate a bunch of client-side functions. diff --git a/doc/tutorial/Docs.lhs b/doc/tutorial/Docs.lhs index 1ae4570e..2aa2c377 100644 --- a/doc/tutorial/Docs.lhs +++ b/doc/tutorial/Docs.lhs @@ -1,7 +1,4 @@ ---- -title: Generating documentation from API types -toc: true ---- +# Generating documentation from API types The source for this tutorial section is a literate haskell file, so first we need to have some language extensions and imports: diff --git a/doc/tutorial/Javascript.lhs b/doc/tutorial/Javascript.lhs index 9098fe8d..69d96547 100644 --- a/doc/tutorial/Javascript.lhs +++ b/doc/tutorial/Javascript.lhs @@ -1,7 +1,4 @@ ---- -title: Deriving Javascript functions to query an API -toc: true ---- +# Deriving Javascript functions to query an API We will now see how *servant* lets you turn an API type into javascript functions that you can call to query a webservice. The derived code assumes you diff --git a/doc/tutorial/index.rst b/doc/tutorial/index.rst index 9044a4d5..92f9ffba 100644 --- a/doc/tutorial/index.rst +++ b/doc/tutorial/index.rst @@ -61,8 +61,8 @@ Tutorial .. toctree:: :maxdepth: 1 - api-type.lhs - server.lhs - client.lhs - javascript.lhs - docs.lhs + ApiType.lhs + Server.lhs + Client.lhs + Javascript.lhs + Docs.lhs From c53945098d0a7f6fc8e042e97ad9ed9b07471df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 28 Jan 2016 12:27:49 +0100 Subject: [PATCH 16/50] tutorial: compile during CI --- doc/tutorial/check/check.sh | 11 ----------- doc/tutorial/check/tinc.yaml | 15 --------------- doc/tutorial/tinc.yaml | 15 +++++++++++++++ sources.txt | 1 + 4 files changed, 16 insertions(+), 26 deletions(-) delete mode 100755 doc/tutorial/check/check.sh delete mode 100644 doc/tutorial/check/tinc.yaml create mode 100644 doc/tutorial/tinc.yaml diff --git a/doc/tutorial/check/check.sh b/doc/tutorial/check/check.sh deleted file mode 100755 index 5425d80a..00000000 --- a/doc/tutorial/check/check.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit - -# tinc - -cabal exec -- ghc -Wall -Werror -outputdir build-output ../api-type.lhs -O0 -c -pgmL markdown-unlit -#cabal exec -- ghc -Wall -Werror -outputdir build-output ../server.lhs -O0 -c -fno-warn-missing-methods -fno-warn-name-shadowing -#cabal exec -- ghc -Wall -Werror -outputdir build-output ../client.lhs -O0 -c -fno-warn-missing-methods -fno-warn-name-shadowing -#cabal exec -- ghc -Wall -Werror -outputdir build-output ../javascript.lhs -O0 -c -fno-warn-missing-methods -#cabal exec -- ghc -Wall -Werror -ibuild-output -outputdir build-output ../docs.lhs -O0 -c -fno-warn-missing-methods diff --git a/doc/tutorial/check/tinc.yaml b/doc/tutorial/check/tinc.yaml deleted file mode 100644 index 2a32c412..00000000 --- a/doc/tutorial/check/tinc.yaml +++ /dev/null @@ -1,15 +0,0 @@ -dependencies: - - name: servant - path: ../../../servant - - name: servant-server - path: ../../../servant-server - - name: servant-client - path: ../../../servant-client - - name: servant-js - path: ../../../servant-js - - name: servant-lucid - path: ../../../servant-lucid - - name: servant-docs - path: ../../../servant-docs - - name: servant-foreign - path: ../../../servant-foreign diff --git a/doc/tutorial/tinc.yaml b/doc/tutorial/tinc.yaml new file mode 100644 index 00000000..b2164752 --- /dev/null +++ b/doc/tutorial/tinc.yaml @@ -0,0 +1,15 @@ +dependencies: + - name: servant + path: ../../servant + - name: servant-server + path: ../../servant-server + - name: servant-client + path: ../../servant-client + - name: servant-js + path: ../../servant-js + - name: servant-lucid + path: ../../servant-lucid + - name: servant-docs + path: ../../servant-docs + - name: servant-foreign + path: ../../servant-foreign diff --git a/sources.txt b/sources.txt index 2d3f8107..2b2ca454 100644 --- a/sources.txt +++ b/sources.txt @@ -8,3 +8,4 @@ servant-server servant-blaze servant-lucid servant-mock +doc/tutorial From 207d51bbf9ae60b1ef718f9d1f7192431fdb62ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 28 Jan 2016 12:47:03 +0100 Subject: [PATCH 17/50] docs: add documentation on how to build the docs locally --- doc/building-the-docs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 doc/building-the-docs diff --git a/doc/building-the-docs b/doc/building-the-docs new file mode 100644 index 00000000..34f8b16f --- /dev/null +++ b/doc/building-the-docs @@ -0,0 +1,8 @@ +To build the docs locally: + +$ virtualenv venv +$ . ./venv/bin/activate +$ pip install -r requirements.txt +$ make html + +Docs will be built in _build/html/index.html . From 4fbf28c3c07f7be72fe5db405377855fceadf8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 28 Jan 2016 12:50:29 +0100 Subject: [PATCH 18/50] tutorial: add markdown-unlit as a cabal dependency --- doc/tutorial/tutorial.cabal | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index 5a907a65..96e31c04 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -44,6 +44,7 @@ library , wai , http-types , transformers + , markdown-unlit >= 0.4 -- hs-source-dirs: default-language: Haskell2010 ghc-options: -Wall -Werror -pgmL markdown-unlit From 21426a223ea295f81d50dab237393add884c8937 Mon Sep 17 00:00:00 2001 From: Andres Loeh Date: Thu, 28 Jan 2016 13:18:12 +0100 Subject: [PATCH 19/50] Rewrite the part on verbs. --- doc/tutorial/ApiType.lhs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/doc/tutorial/ApiType.lhs b/doc/tutorial/ApiType.lhs index 28ecb5e8..8c2629f9 100644 --- a/doc/tutorial/ApiType.lhs +++ b/doc/tutorial/ApiType.lhs @@ -88,19 +88,30 @@ type UserAPI3 = "users" :> "list-all" :> "now" :> Get '[JSON] [User] ### `Delete`, `Get`, `Patch`, `Post` and `Put` -These 5 combinators are very similar except that they each describe a -different HTTP method. This is how they're declared - +The `Get` combinator is defined in terms of the more general `Verb`: ``` haskell ignore -data Delete (contentTypes :: [*]) a -data Get (contentTypes :: [*]) a -data Patch (contentTypes :: [*]) a -data Post (contentTypes :: [*]) a -data Put (contentTypes :: [*]) a +data Verb method (statusCode :: Nat) (contentType :: [*]) a +type Get = Verb 'GET 200 ``` -An endpoint ends with one of the 5 combinators above (unless you write your -own). Examples: +There are other predefined type synonyms for other common HTTP methods, +such as e.g.: +``` haskell ignore +data Delete = Verb 'DELETE 200 +data Patch = Verb 'PATCH 200 +data Post = Verb 'POST 200 +data Put = Verb 'PUT 200 +``` + +There are also variants that do not return a 200 status code, such +as for example: +``` haskell ignore +type PostCreated = Verb 'POST 201 +type PostAccepted = Verb 'POST 202 +``` + +An endpoint always ends with a variant of the `Verb` combinator +(unless you write your own combinators). Examples: ``` haskell type UserAPI4 = "users" :> Get '[JSON] [User] From 52b1a233fe4a0ea406963157dfed8dd138636279 Mon Sep 17 00:00:00 2001 From: Andres Loeh Date: Thu, 28 Jan 2016 13:30:58 +0100 Subject: [PATCH 20/50] Add / expand the documentation of the NoContent case. --- doc/tutorial/ApiType.lhs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/tutorial/ApiType.lhs b/doc/tutorial/ApiType.lhs index 8c2629f9..bbe1da43 100644 --- a/doc/tutorial/ApiType.lhs +++ b/doc/tutorial/ApiType.lhs @@ -149,10 +149,15 @@ type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User -- except that we explicitly say that "userid" -- must be an integer - :<|> "user" :> Capture "userid" Integer :> Delete '[] () + :<|> "user" :> Capture "userid" Integer :> DeleteNoContent '[JSON] NoContent -- equivalent to 'DELETE /user/:userid' ``` +In the second case, `DeleteNoContent` specifies a 204 response code, +`JSON` specifies the content types on which the handler will match, +and `NoContent` is a Haskell type isomorphic to `()` used to represent +a trivial piece of information. + ### `QueryParam`, `QueryParams`, `QueryFlag` `QueryParam`, `QueryParams` and `QueryFlag` are about query string From df363cecb095410a7a5d87f5f85a3f62f33af964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 28 Jan 2016 13:22:20 +0100 Subject: [PATCH 21/50] tutorial: make Client.lhs compile --- doc/tutorial/Client.lhs | 29 ++++++++++++++++++----------- doc/tutorial/tutorial.cabal | 5 ++++- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/doc/tutorial/Client.lhs b/doc/tutorial/Client.lhs index 1a186596..ea8bdbfa 100644 --- a/doc/tutorial/Client.lhs +++ b/doc/tutorial/Client.lhs @@ -15,12 +15,14 @@ need to have some language extensions and imports: module Client where -import Control.Monad.Trans.Either +import Control.Monad.Trans.Except import Data.Aeson import Data.Proxy import GHC.Generics +import Network.HTTP.Client (Manager, newManager, defaultManagerSettings) import Servant.API import Servant.Client +import System.IO.Unsafe ``` Also, we need examples for some domain specific data types: @@ -70,13 +72,13 @@ What we are going to get with *servant-client* here is 3 functions, one to query ``` haskell position :: Int -- ^ value for "x" -> Int -- ^ value for "y" - -> EitherT ServantError IO Position + -> ExceptT ServantError IO Position hello :: Maybe String -- ^ an optional value for "name" - -> EitherT ServantError IO HelloMessage + -> ExceptT ServantError IO HelloMessage marketing :: ClientInfo -- ^ value for the request body - -> EitherT ServantError IO Email + -> ExceptT ServantError IO Email ``` Each function makes available as an argument any value that the response may depend on, as evidenced in the API type. How do we get these functions? Just give a `Proxy` to your API and a host to make the requests to: @@ -85,7 +87,12 @@ Each function makes available as an argument any value that the response may dep api :: Proxy API api = Proxy -position :<|> hello :<|> marketing = client api (BaseUrl Http "localhost" 8081) +{-# NOINLINE __manager #-} +__manager :: Manager +__manager = unsafePerformIO $ newManager defaultManagerSettings + +position :<|> hello :<|> marketing = + client api (BaseUrl Http "localhost" 8081 "") __manager ``` As you can see in the code above, we just "pattern match our way" to these functions. If we try to derive less or more functions than there are endpoints in the API, we obviously get an error. The `BaseUrl` value there is just: @@ -109,21 +116,21 @@ data BaseUrl = BaseUrl That's it. Let's now write some code that uses our client functions. ``` haskell -queries :: EitherT ServantError IO (Position, HelloMessage, Email) +queries :: ExceptT ServantError IO (Position, HelloMessage, Email) queries = do pos <- position 10 10 - msg <- hello (Just "servant") + message <- hello (Just "servant") em <- marketing (ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]) - return (pos, msg, em) + return (pos, message, em) run :: IO () run = do - res <- runEitherT queries + res <- runExceptT queries case res of Left err -> putStrLn $ "Error: " ++ show err - Right (pos, msg, em) -> do + Right (pos, message, em) -> do print pos - print msg + print message print em ``` diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index 96e31c04..628ca87f 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -15,7 +15,7 @@ cabal-version: >=1.10 library exposed-modules: ApiType - -- , Client + , Client -- , Docs -- , Javascript -- , Server @@ -45,6 +45,9 @@ library , http-types , transformers , markdown-unlit >= 0.4 + , http-client -- hs-source-dirs: default-language: Haskell2010 ghc-options: -Wall -Werror -pgmL markdown-unlit + -- to silence aeson-0.10 warnings: + ghc-options: -fno-warn-missing-methods From ad48c0efa612451ca753b0c7f7df61be91bbdcbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 28 Jan 2016 13:26:41 +0100 Subject: [PATCH 22/50] tutorial: allow older ghcs in cabal file --- doc/tutorial/tutorial.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index 628ca87f..e6ec21c9 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -21,7 +21,7 @@ library -- , Server -- other-modules: -- other-extensions: - build-depends: base >=4.8 && <4.9 + build-depends: base == 4.* , text , aeson , blaze-html From a7424c47530f8fbc053b5dc2829330242372f62f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 28 Jan 2016 14:12:24 +0100 Subject: [PATCH 23/50] tutorial: make Server compile --- doc/tutorial/Server.lhs | 123 ++++++++++++++++-------------------- doc/tutorial/tutorial.cabal | 3 +- 2 files changed, 56 insertions(+), 70 deletions(-) diff --git a/doc/tutorial/Server.lhs b/doc/tutorial/Server.lhs index 29ca6cb8..56add0ab 100644 --- a/doc/tutorial/Server.lhs +++ b/doc/tutorial/Server.lhs @@ -48,7 +48,7 @@ module Server where import Control.Monad.IO.Class import Control.Monad.Reader -import Control.Monad.Trans.Either +import Control.Monad.Trans.Except import Data.Aeson import Data.Aeson.Types import Data.Attoparsec.ByteString @@ -130,11 +130,11 @@ corresponding API type. The first thing to know about the `Server` type family is that behind the scenes it will drive the routing, letting you focus only on the business logic. The second thing to know is that for each endpoint, your handlers will -by default run in the `EitherT ServantErr IO` monad. This is overridable very +by default run in the `ExceptT ServantErr IO` monad. This is overridable very easily, as explained near the end of this guide. Third thing, the type of the value returned in that monad must be the same as the second argument of the HTTP method combinator used for the corresponding endpoint. In our case, it -means we must provide a handler of type `EitherT ServantErr IO [User]`. Well, +means we must provide a handler of type `ExceptT ServantErr IO [User]`. Well, we have a monad, let's just `return` our list: ``` haskell @@ -152,7 +152,7 @@ userAPI = Proxy -- which you can think of as an "abstract" web application, -- not yet a webserver. app1 :: Application -app1 = serve userAPI server1 +app1 = serve userAPI EmptyConfig server1 ``` The `userAPI` bit is, alas, boilerplate (we need it to guide type inference). @@ -288,15 +288,15 @@ server3 = position :<|> hello :<|> marketing - where position :: Int -> Int -> EitherT ServantErr IO Position + where position :: Int -> Int -> ExceptT ServantErr IO Position position x y = return (Position x y) - hello :: Maybe String -> EitherT ServantErr IO HelloMessage + hello :: Maybe String -> ExceptT ServantErr IO HelloMessage hello mname = return . HelloMessage $ case mname of Nothing -> "Hello, anonymous coward" Just n -> "Hello, " ++ n - marketing :: ClientInfo -> EitherT ServantErr IO Email + marketing :: ClientInfo -> ExceptT ServantErr IO Email marketing clientinfo = return (emailForClient clientinfo) ``` @@ -327,7 +327,7 @@ $ curl -X POST -d '{"name":"Alp Mestanogullari", "email" : "alp@foo.com", "age": For reference, here's a list of some combinators from *servant* and for those that get turned into arguments to the handlers, the type of the argument. - > - `Delete`, `Get`, `Patch`, `Post`, `Put`: these do not become arguments. They provide the return type of handlers, which usually is `EitherT ServantErr IO `. + > - `Delete`, `Get`, `Patch`, `Post`, `Put`: these do not become arguments. They provide the return type of handlers, which usually is `ExceptT ServantErr IO `. > - `Capture "something" a` becomes an argument of type `a`. > - `QueryParam "something" a`, `MatrixParam "something" a`, `Header "something" a` all become arguments of type `Maybe a`, because there might be no value at all specified by the client for these. > - `QueryFlag "something"` and `MatrixFlag "something"` get turned into arguments of type `Bool`. @@ -369,22 +369,7 @@ data Direction | Left | Right -instance FromText Direction where - -- requires {-# LANGUAGE OverloadedStrings #-} - fromText "up" = Just Up - fromText "down" = Just Down - fromText "left" = Just Server.Left - fromText "right" = Just Server.Right - fromText _ = Nothing - -instance ToText Direction where - toText Up = "up" - toText Down = "down" - toText Server.Left = "left" - toText Server.Right = "right" - newtype UserId = UserId Int64 - deriving (FromText, ToText) ``` or writing the instances by hand: @@ -643,7 +628,7 @@ server4 :: Server PersonAPI server4 = return persons app2 :: Application -app2 = serve personAPI server4 +app2 = serve personAPI EmptyConfig server4 ``` And we're good to go. You can run this example with `dist/build/tutorial/tutorial 4`. @@ -656,10 +641,10 @@ And we're good to go. You can run this example with `dist/build/tutorial/tutoria # or just point your browser to http://localhost:8081/persons ``` -The `EitherT ServantErr IO` monad +The `ExceptT ServantErr IO` monad ================================= -At the heart of the handlers is the monad they run in, namely `EitherT +At the heart of the handlers is the monad they run in, namely `ExceptT ServantErr IO`. One might wonder: why this monad? The answer is that it is the simplest monad with the following properties: @@ -677,11 +662,11 @@ data Either e a = Left e | Right a -- from the 'either' package at -- http://hackage.haskell.org/package/either-4.3.3.2/docs/Control-Monad-Trans-Either.html -newtype EitherT e m a - = EitherT { runEitherT :: m (Either e a) } +newtype ExceptT e m a + = ExceptT { runEitherT :: m (Either e a) } ``` -In short, this means that a handler of type `EitherT ServantErr IO a` is simply +In short, this means that a handler of type `ExceptT ServantErr IO a` is simply equivalent to a computation of type `IO (Either ServantErr a)`, that is, an IO action that either returns an error or a result. @@ -689,7 +674,7 @@ The aforementioned `either` package is worth taking a look at. Perhaps most importantly: ``` haskell ignore -left :: Monad m => e -> EitherT e m a +left :: Monad m => e -> ExceptT e m a ``` Allows you to return an error from your handler (whereas `return` is enough to return a success). @@ -701,14 +686,14 @@ kind and abort early. The next two sections cover how to do just that. Performing IO ------------- -Another important instance from the list above is `MonadIO m => MonadIO (EitherT e m)`. [`MonadIO`](http://hackage.haskell.org/package/transformers-0.4.3.0/docs/Control-Monad-IO-Class.html) is a class from the *transformers* package defined as: +Another important instance from the list above is `MonadIO m => MonadIO (ExceptT e m)`. [`MonadIO`](http://hackage.haskell.org/package/transformers-0.4.3.0/docs/Control-Monad-IO-Class.html) is a class from the *transformers* package defined as: ``` haskell ignore class Monad m => MonadIO m where liftIO :: IO a -> m a ``` -Obviously, the `IO` monad provides a `MonadIO` instance. Hence for any type `e`, `EitherT e IO` has a `MonadIO` instance. So if you want to run any kind of IO computation in your handlers, just use `liftIO`: +Obviously, the `IO` monad provides a `MonadIO` instance. Hence for any type `e`, `ExceptT e IO` has a `MonadIO` instance. So if you want to run any kind of IO computation in your handlers, just use `liftIO`: ``` haskell type IOAPI1 = "myfile.txt" :> Get '[JSON] FileContent @@ -748,8 +733,8 @@ module. If you want to use these values but add a body or some headers, just use record update syntax: ``` haskell -failingHandler :: EitherT ServantErr IO () -failingHandler = left myerr +failingHandler :: ExceptT ServantErr IO () +failingHandler = throwE myerr where myerr :: ServantErr myerr = err503 { errBody = "Sorry dear user." } @@ -764,7 +749,7 @@ server6 = do exists <- liftIO (doesFileExist "myfile.txt") if exists then liftIO (readFile "myfile.txt") >>= return . FileContent - else left custom404Err + else throwE custom404Err where custom404Err = err404 { errBody = "myfile.txt just isn't there, please leave this server alone." } ``` @@ -854,7 +839,7 @@ server7 :: Server CodeAPI server7 = serveDirectory "tutorial" app3 :: Application -app3 = serve codeAPI server7 +app3 = serve codeAPI EmptyConfig server7 ``` This server will match any request whose path starts with `/code` and will look for a file at the path described by the rest of the request path, inside the *tutorial/* directory of the path you run the program from. @@ -981,25 +966,25 @@ type UserAPI4 = Capture "userid" Int :> However, you have to be aware that this has an effect on the type of the corresponding `Server`: ``` haskell ignore -Server UserAPI3 = (Int -> EitherT ServantErr IO User) - :<|> (Int -> EitherT ServantErr IO ()) +Server UserAPI3 = (Int -> ExceptT ServantErr IO User) + :<|> (Int -> ExceptT ServantErr IO ()) -Server UserAPI4 = Int -> ( EitherT ServantErr IO User - :<|> EitherT ServantErr IO () +Server UserAPI4 = Int -> ( ExceptT ServantErr IO User + :<|> ExceptT ServantErr IO () ) ``` In the first case, each handler receives the *userid* argument. In the latter, -the whole `Server` takes the *userid* and has handlers that are just computations in `EitherT`, with no arguments. In other words: +the whole `Server` takes the *userid* and has handlers that are just computations in `ExceptT`, with no arguments. In other words: ``` haskell server8 :: Server UserAPI3 server8 = getUser :<|> deleteUser - where getUser :: Int -> EitherT ServantErr IO User + where getUser :: Int -> ExceptT ServantErr IO User getUser _userid = error "..." - deleteUser :: Int -> EitherT ServantErr IO () + deleteUser :: Int -> ExceptT ServantErr IO () deleteUser _userid = error "..." -- notice how getUser and deleteUser @@ -1008,10 +993,10 @@ server8 = getUser :<|> deleteUser server9 :: Server UserAPI4 server9 userid = getUser userid :<|> deleteUser userid - where getUser :: Int -> EitherT ServantErr IO User + where getUser :: Int -> ExceptT ServantErr IO User getUser = error "..." - deleteUser :: Int -> EitherT ServantErr IO () + deleteUser :: Int -> ExceptT ServantErr IO () deleteUser = error "..." ``` @@ -1055,23 +1040,23 @@ type UsersAPI = usersServer :: Server UsersAPI usersServer = getUsers :<|> newUser :<|> userOperations - where getUsers :: EitherT ServantErr IO [User] + where getUsers :: ExceptT ServantErr IO [User] getUsers = error "..." - newUser :: User -> EitherT ServantErr IO () + newUser :: User -> ExceptT ServantErr IO () newUser = error "..." userOperations userid = viewUser userid :<|> updateUser userid :<|> deleteUser userid where - viewUser :: Int -> EitherT ServantErr IO User + viewUser :: Int -> ExceptT ServantErr IO User viewUser = error "..." - updateUser :: Int -> User -> EitherT ServantErr IO () + updateUser :: Int -> User -> ExceptT ServantErr IO () updateUser = error "..." - deleteUser :: Int -> EitherT ServantErr IO () + deleteUser :: Int -> ExceptT ServantErr IO () deleteUser = error "..." ``` @@ -1090,23 +1075,23 @@ data Product = Product { productId :: Int } productsServer :: Server ProductsAPI productsServer = getProducts :<|> newProduct :<|> productOperations - where getProducts :: EitherT ServantErr IO [Product] + where getProducts :: ExceptT ServantErr IO [Product] getProducts = error "..." - newProduct :: Product -> EitherT ServantErr IO () + newProduct :: Product -> ExceptT ServantErr IO () newProduct = error "..." productOperations productid = viewProduct productid :<|> updateProduct productid :<|> deleteProduct productid where - viewProduct :: Int -> EitherT ServantErr IO Product + viewProduct :: Int -> ExceptT ServantErr IO Product viewProduct = error "..." - updateProduct :: Int -> Product -> EitherT ServantErr IO () + updateProduct :: Int -> Product -> ExceptT ServantErr IO () updateProduct = error "..." - deleteProduct :: Int -> EitherT ServantErr IO () + deleteProduct :: Int -> ExceptT ServantErr IO () deleteProduct = error "..." ``` @@ -1134,11 +1119,11 @@ type APIFor a i = -- Build the appropriate 'Server' -- given the handlers of the right type. -serverFor :: EitherT ServantErr IO [a] -- handler for listing of 'a's - -> (a -> EitherT ServantErr IO ()) -- handler for adding an 'a' - -> (i -> EitherT ServantErr IO a) -- handler for viewing an 'a' given its identifier of type 'i' - -> (i -> a -> EitherT ServantErr IO ()) -- updating an 'a' with given id - -> (i -> EitherT ServantErr IO ()) -- deleting an 'a' given its id +serverFor :: ExceptT ServantErr IO [a] -- handler for listing of 'a's + -> (a -> ExceptT ServantErr IO ()) -- handler for adding an 'a' + -> (i -> ExceptT ServantErr IO a) -- handler for viewing an 'a' given its identifier of type 'i' + -> (i -> a -> ExceptT ServantErr IO ()) -- updating an 'a' with given id + -> (i -> ExceptT ServantErr IO ()) -- deleting an 'a' given its id -> Server (APIFor a i) serverFor = error "..." -- implementation left as an exercise. contact us on IRC @@ -1148,10 +1133,10 @@ serverFor = error "..." Using another monad for your handlers ===================================== -Remember how `Server` turns combinators for HTTP methods into `EitherT ServantErr IO`? Well, actually, there's more to that. `Server` is actually a simple type synonym. +Remember how `Server` turns combinators for HTTP methods into `ExceptT ServantErr IO`? Well, actually, there's more to that. `Server` is actually a simple type synonym. ``` haskell ignore -type Server api = ServerT api (EitherT ServantErr IO) +type Server api = ServerT api (ExceptT ServantErr IO) ``` `ServerT` is the actual type family that computes the required types for the handlers that's part of the `HasServer` class. It's like `Server` except that it takes a third parameter which is the monad you want your handlers to run in, or more generally the return types of your handlers. This third parameter is used for specifying the return type of the handler for an endpoint, e.g when computing `ServerT (Get '[JSON] Person) SomeMonad`. The result would be `SomeMonad Person`. @@ -1173,24 +1158,24 @@ newtype m :~> n = Nat { unNat :: forall a. m a -> n a} ``` (`Nat` comes from "natural transformation", in case you're wondering.) -So if you want to write handlers using another monad/type than `EitherT +So if you want to write handlers using another monad/type than `ExceptT ServantErr IO`, say the `Reader String` monad, the first thing you have to prepare is a function: ``` haskell ignore -readerToEither :: Reader String :~> EitherT ServantErr IO +readerToEither :: Reader String :~> ExceptT ServantErr IO ``` Let's start with `readerToEither'`. We obviously have to run the `Reader` computation by supplying it with a `String`, like `"hi"`. We get an `a` out -from that and can then just `return` it into `EitherT`. We can then just wrap +from that and can then just `return` it into `ExceptT`. We can then just wrap that function with the `Nat` constructor to make it have the fancier type. ``` haskell -readerToEither' :: forall a. Reader String a -> EitherT ServantErr IO a +readerToEither' :: forall a. Reader String a -> ExceptT ServantErr IO a readerToEither' r = return (runReader r "hi") -readerToEither :: Reader String :~> EitherT ServantErr IO +readerToEither :: Reader String :~> ExceptT ServantErr IO readerToEither = Nat readerToEither' ``` @@ -1214,7 +1199,7 @@ readerServerT = a :<|> b ``` We unfortunately can't use `readerServerT` as an argument of `serve`, because -`serve` wants a `Server ReaderAPI`, i.e., with handlers running in `EitherT +`serve` wants a `Server ReaderAPI`, i.e., with handlers running in `ExceptT ServantErr IO`. But there's a simple solution to this. Enter `enter` @@ -1233,7 +1218,7 @@ readerServer :: Server ReaderAPI readerServer = enter readerToEither readerServerT app4 :: Application -app4 = serve readerAPI readerServer +app4 = serve readerAPI EmptyConfig readerServer ``` And we can indeed see this webservice in action by running `dist/build/tutorial/tutorial 7`. diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index e6ec21c9..54f24096 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -18,7 +18,7 @@ library , Client -- , Docs -- , Javascript - -- , Server + , Server -- other-modules: -- other-extensions: build-depends: base == 4.* @@ -51,3 +51,4 @@ library ghc-options: -Wall -Werror -pgmL markdown-unlit -- to silence aeson-0.10 warnings: ghc-options: -fno-warn-missing-methods + ghc-options: -fno-warn-name-shadowing From 6cb529fc5fd43a07b59efae380a204e614232b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 28 Jan 2016 14:16:05 +0100 Subject: [PATCH 24/50] tutorial: added working .ghci --- doc/tutorial/.ghci | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/tutorial/.ghci diff --git a/doc/tutorial/.ghci b/doc/tutorial/.ghci new file mode 100644 index 00000000..7d8e760c --- /dev/null +++ b/doc/tutorial/.ghci @@ -0,0 +1 @@ +:set -pgmL markdown-unlit -Wall -Werror -fno-warn-missing-methods -fno-warn-name-shadowing From 678d50796ba368c2b5188f3ef5b6068351b97180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 28 Jan 2016 14:39:54 +0100 Subject: [PATCH 25/50] tutorial: fix for ghc-7.8 --- doc/tutorial/Server.lhs | 3 +++ doc/tutorial/tutorial.cabal | 1 + 2 files changed, 4 insertions(+) diff --git a/doc/tutorial/Server.lhs b/doc/tutorial/Server.lhs index 56add0ab..a6b7696c 100644 --- a/doc/tutorial/Server.lhs +++ b/doc/tutorial/Server.lhs @@ -46,6 +46,9 @@ need to have some language extensions and imports: module Server where +import Prelude () +import Prelude.Compat + import Control.Monad.IO.Class import Control.Monad.Reader import Control.Monad.Trans.Except diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index 54f24096..c22fd8aa 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -22,6 +22,7 @@ library -- other-modules: -- other-extensions: build-depends: base == 4.* + , base-compat , text , aeson , blaze-html From 7445d56c6808d0300915d3ea44c3fac567cfcc7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 28 Jan 2016 15:46:56 +0100 Subject: [PATCH 26/50] tutorial: make Docs.lhs compile --- doc/tutorial/Docs.lhs | 16 ++++++++-------- doc/tutorial/tutorial.cabal | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/tutorial/Docs.lhs b/doc/tutorial/Docs.lhs index 2aa2c377..c006c0b1 100644 --- a/doc/tutorial/Docs.lhs +++ b/doc/tutorial/Docs.lhs @@ -61,8 +61,8 @@ instance ToCapture (Capture "y" Int) where DocCapture "y" -- name "(integer) position on the y axis" -- description -instance ToSample Position Position where - toSample _ = Just (Position 3 14) -- example of output +instance ToSample Position where + toSamples _ = singleSample (Position 3 14) -- example of output instance ToParam (QueryParam "name" String) where toParam _ = @@ -71,7 +71,7 @@ instance ToParam (QueryParam "name" String) where "Name of the person to say hello to." -- description Normal -- Normal, List or Flag -instance ToSample HelloMessage HelloMessage where +instance ToSample HelloMessage where toSamples _ = [ ("When a value is provided for 'name'", HelloMessage "Hello, Alp") , ("When 'name' is not specified", HelloMessage "Hello, anonymous coward") @@ -81,11 +81,11 @@ instance ToSample HelloMessage HelloMessage where ci :: ClientInfo ci = ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"] -instance ToSample ClientInfo ClientInfo where - toSample _ = Just ci +instance ToSample ClientInfo where + toSamples _ = singleSample ci -instance ToSample Email Email where - toSample _ = Just (emailForClient ci) +instance ToSample Email where + toSamples _ = singleSample (emailForClient ci) ``` Types that are used as request or response bodies have to instantiate the `ToSample` typeclass which lets you specify one or more examples of values. `Capture`s and `QueryParam`s have to instantiate their respective `ToCapture` and `ToParam` classes and provide a name and some information about the concrete meaning of that argument, as illustrated in the code above. @@ -228,7 +228,7 @@ server = Server.server3 :<|> serveDocs plain = ("Content-Type", "text/plain") app :: Application -app = serve api server +app = serve api EmptyConfig server ``` And if you spin up this server with `dist/build/tutorial/tutorial 10` and go to anywhere else than `/position`, `/hello` and `/marketing`, you will see the API docs in markdown. This is because `serveDocs` is attempted if the 3 other endpoints don't match and systematically succeeds since its definition is to just return some fixed bytestring with the `text/plain` content type. diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index c22fd8aa..501dace4 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -16,7 +16,7 @@ cabal-version: >=1.10 library exposed-modules: ApiType , Client - -- , Docs + , Docs -- , Javascript , Server -- other-modules: From 487746f9e0c62647398b3ce48fc181286afd4338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 28 Jan 2016 18:44:37 +0100 Subject: [PATCH 27/50] tutorial: make Javascript.lhs compile --- doc/tutorial/Javascript.lhs | 17 +++++++++-------- doc/tutorial/tinc.yaml | 2 -- doc/tutorial/tutorial.cabal | 3 ++- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/tutorial/Javascript.lhs b/doc/tutorial/Javascript.lhs index 69d96547..9ab4740e 100644 --- a/doc/tutorial/Javascript.lhs +++ b/doc/tutorial/Javascript.lhs @@ -30,13 +30,14 @@ module Javascript where import Control.Monad.IO.Class import Data.Aeson import Data.Proxy -import Data.Text (Text) +import Data.Text as T (Text) +import Data.Text.IO as T (writeFile, readFile) import qualified Data.Text as T import GHC.Generics import Language.Javascript.JQuery import Network.Wai import Servant -import Servant.JQuery +import Servant.JS import System.Random ``` @@ -133,7 +134,7 @@ server' = server :<|> serveDirectory "tutorial/t9" app :: Application -app = serve api' server' +app = serve api' EmptyConfig server' ``` Why two different API types, proxies and servers though? Simply because we don't want to generate javascript functions for the `Raw` part of our API type, so we need a `Proxy` for our API type `API'` without its `Raw` endpoint. @@ -141,8 +142,8 @@ Why two different API types, proxies and servers though? Simply because we don't Very similarly to how one can derive haskell functions, we can derive the javascript with just a simple function call to `jsForAPI` from `Servant.JQuery`. ``` haskell -apiJS :: String -apiJS = jsForAPI api +apiJS :: Text +apiJS = jsForAPI api vanillaJS ``` This `String` contains 2 Javascript functions: @@ -175,9 +176,9 @@ Right before starting up our server, we will need to write this `String` to a fi ``` haskell writeJSFiles :: IO () writeJSFiles = do - writeFile "getting-started/gs9/api.js" apiJS - jq <- readFile =<< Language.Javascript.JQuery.file - writeFile "getting-started/gs9/jq.js" jq + T.writeFile "getting-started/gs9/api.js" apiJS + jq <- T.readFile =<< Language.Javascript.JQuery.file + T.writeFile "getting-started/gs9/jq.js" jq ``` And we're good to go. Start the server with `dist/build/tutorial/tutorial 9` and go to `http://localhost:8081/`. Start typing in the name of one of the authors in our database or part of a book title, and check out how long it takes to approximate π using the method mentioned above. diff --git a/doc/tutorial/tinc.yaml b/doc/tutorial/tinc.yaml index b2164752..f52bab2d 100644 --- a/doc/tutorial/tinc.yaml +++ b/doc/tutorial/tinc.yaml @@ -7,8 +7,6 @@ dependencies: path: ../../servant-client - name: servant-js path: ../../servant-js - - name: servant-lucid - path: ../../servant-lucid - name: servant-docs path: ../../servant-docs - name: servant-foreign diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index 501dace4..9c3d664b 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -17,7 +17,7 @@ library exposed-modules: ApiType , Client , Docs - -- , Javascript + , Javascript , Server -- other-modules: -- other-extensions: @@ -32,6 +32,7 @@ library , servant-server == 0.5.* , servant-client == 0.5.* , servant-docs == 0.5.* + , servant-js == 0.5.* , warp , http-media , lucid From 73ab3062abe1168505e477c0c1dfc618850d36cc Mon Sep 17 00:00:00 2001 From: Oleg Grenrus Date: Sat, 6 Feb 2016 13:33:53 +0200 Subject: [PATCH 28/50] Use rst links syntax (it's not a markdown) --- doc/tutorial/index.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/tutorial/index.rst b/doc/tutorial/index.rst index 92f9ffba..ab212368 100644 --- a/doc/tutorial/index.rst +++ b/doc/tutorial/index.rst @@ -1,13 +1,15 @@ Servant tutorial ================ -This is an introductory tutorial to the current version of *servant*, which is **0.4**. Any comment or issue can be directed to [this website's issue tracker](http://github.com/haskell-servant/haskell-servant.github.io/issues). +This is an introductory tutorial to the current version of *servant*, which is +**0.4**. Any comment or issue can be directed to `this website's issue +tracker `_. Github ------- -- the servant packages: [haskell-servant/servant](https://github.com/haskell-servant/servant) -- the website (including this tutorial): [haskell-servant/haskell-servant.github.io](https://github.com/haskell-servant/haskell-servant.github.io/) +- the servant packages: `haskell-servant/servant `_ +- the website (including this tutorial): `haskell-servant/haskell-servant.github.io `_ - Feel free to use the issue tracker (or to send PRs!) on the website's repository to give feedback and suggestions about this tutorial Introduction From 9d2d7104d0cf0c9c9ca14a83cea63e5db80d96eb Mon Sep 17 00:00:00 2001 From: rwobben Date: Tue, 9 Feb 2016 11:56:23 +0000 Subject: [PATCH 29/50] Changed so two titles are displayed correctly --- doc/tutorial/Server.lhs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/doc/tutorial/Server.lhs b/doc/tutorial/Server.lhs index a6b7696c..5c0e9f2d 100644 --- a/doc/tutorial/Server.lhs +++ b/doc/tutorial/Server.lhs @@ -1,7 +1,4 @@ ---- -title: Serving an API -toc: true ---- +# Serving an API Enough chit-chat about type-level combinators and representing an API as a type. Can we have a webservice already? From e1312c1bb6abb8e89bbfff30122db74ad8cb2e39 Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Thu, 18 Feb 2016 00:39:40 +0100 Subject: [PATCH 30/50] sundry tutorial improvements --- doc/conf.py | 5 ++ doc/requirements.txt | 2 +- doc/tutorial/Server.lhs | 159 +++++++++++++++--------------------- doc/tutorial/tutorial.cabal | 2 +- stack.yaml | 3 + 5 files changed, 76 insertions(+), 95 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 4e31a37d..1c7aba02 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -94,6 +94,11 @@ exclude_patterns = ['_build', 'venv'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' +def setup(app): + from sphinx.highlighting import lexers + from pygments.lexers import HaskellLexer + lexers['haskell ignore'] = HaskellLexer(stripnl=False) + # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] diff --git a/doc/requirements.txt b/doc/requirements.txt index 8f89e4b8..0c9c95a8 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -10,7 +10,7 @@ Jinja2==2.8 livereload==2.4.1 MarkupSafe==0.23 pathtools==0.1.2 -Pygments==2.1 +Pygments==2.1.1 pytz==2015.7 PyYAML==3.11 recommonmark==0.4.0 diff --git a/doc/tutorial/Server.lhs b/doc/tutorial/Server.lhs index 5c0e9f2d..79588da7 100644 --- a/doc/tutorial/Server.lhs +++ b/doc/tutorial/Server.lhs @@ -23,8 +23,7 @@ Usage: tutorial N where N is the number of the example you want to run. ``` -A first example -=============== +## A first example Equipped with some basic knowledge about the way we represent API, let's now write our first webservice. @@ -53,7 +52,6 @@ import Data.Aeson import Data.Aeson.Types import Data.Attoparsec.ByteString import Data.ByteString (ByteString) -import Data.Int import Data.List import Data.String.Conversions import Data.Time.Calendar @@ -97,7 +95,7 @@ data User = User { name :: String , age :: Int , email :: String - , registration_date :: Day + , registrationDate :: Day } deriving (Eq, Show, Generic) instance ToJSON User @@ -179,8 +177,7 @@ $ curl http://localhost:8081/users [{"email":"isaac@newton.co.uk","registration_date":"1683-03-01","age":372,"name":"Isaac Newton"},{"email":"ae@mc2.org","registration_date":"1905-12-01","age":136,"name":"Albert Einstein"}] ``` -More endpoints -============== +## More endpoints What if we want more than one endpoint? Let's add `/albert` and `/isaac` to view the corresponding users encoded in JSON. @@ -218,8 +215,7 @@ And that's it! You can run this example with `dist/build/tutorial/tutorial 2` and check out the data available at `/users`, `/albert` and `/isaac`. -From combinators to handler arguments -===================================== +## From combinators to handler arguments Fine, we can write trivial webservices easily, but none of the two above use any "fancy" combinator from servant. Let's address this and use `QueryParam`, @@ -237,8 +233,8 @@ type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Posit :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email data Position = Position - { x :: Int - , y :: Int + { xCoord :: Int + , yCoord :: Int } deriving Generic instance ToJSON Position @@ -334,8 +330,7 @@ that get turned into arguments to the handlers, the type of the argument. > - `QueryParams "something" a` and `MatrixParams "something" a` get turned into arguments of type `[a]`. > - `ReqBody contentTypes a` gets turned into an argument of type `a`. -The `FromText`/`ToText` classes -=============================== +## The `FromHttpApiData`/`ToHttpApiData` classes Wait... How does *servant* know how to decode the `Int`s from the URL? Or how to decode a `ClientInfo` value from the request body? This is what this and the @@ -343,54 +338,42 @@ following two sections address. `Capture`s and `QueryParam`s are represented by some textual value in URLs. `Header`s are similarly represented by a pair of a header name and a -corresponding (textual) value in the request's "metadata". This is why we -decided to provide a pair of typeclasses, `FromText` and `ToText` which just -let you say that you can respectively *extract* or *encode* values of some type -*from*/*to* text. Here are the definitions: +corresponding (textual) value in the request's "metadata". How types are +decoded from headers, captures, and query params is expressed in a class +`FromHttpApiData` (from the package +[*http-api-data*](http://hackage.haskell.org/package/http-api-data)): ``` haskell ignore -class FromText a where - fromText :: Text -> Maybe a +class FromHttpApiData a where + {-# MINIMAL parseUrlPiece | parseQueryParam #-} + -- | Parse URL path piece. + parseUrlPiece :: Text -> Either Text a + parseUrlPiece = parseQueryParam -class ToText a where - toText :: a -> Text + -- | Parse HTTP header value. + parseHeader :: ByteString -> Either Text a + parseHeader = parseUrlPiece . decodeUtf8 + + -- | Parse query param value. + parseQueryParam :: Text -> Either Text a + parseQueryParam = parseUrlPiece ``` -And as long as the type that a `Capture`/`QueryParam`/`Header`/etc will be -decoded to provides a `FromText` instance, it will Just Work. *servant* -provides a decent number of instances, but here are some examples of defining -your own. +As you can see, as long as you provide either `parseUrlPiece` (for `Capture`s) +or `parseQueryParam` (for `QueryParam`s), the other methods will be defined in +terms of this. -``` haskell --- A typical enumeration -data Direction - = Up - | Down - | Left - | Right - -newtype UserId = UserId Int64 -``` - -or writing the instances by hand: - -``` haskell ignore -instance FromText UserId where - fromText = fmap UserId fromText - -instance ToText UserId where - toText (UserId i) = toText i -``` +*http-api-data* provides a decent number of instances, helpers for defining new +ones, and wonderful documentation. There's not much else to say about these classes. You will need instances for -them when using `Capture`, `QueryParam`, `QueryParams`, `MatrixParam`, -`MatrixParams` and `Header` with your types. You will need `FromText` instances -for server-side request handlers and `ToText` instances only when using +them when using `Capture`, `QueryParam`, `QueryParams`, and `Header` with your +types. You will need `FromHttpApiData` instances for server-side request +handlers and `ToHttpApiData` instances only when using *servant-client*, as described in the [section about deriving haskell functions to query an API](/tutorial/client.html). -Using content-types with your data types -======================================== +## Using content-types with your data types The same principle was operating when decoding request bodies from JSON, and responses *into* JSON. (JSON is just the running example - you can do this with @@ -399,8 +382,8 @@ any content-type.) This section introduces a couple of typeclasses provided by *servant* that make all of this work. -The truth behind `JSON` ------------------------ +### The truth behind `JSON` + What exactly is `JSON`? Like the 3 other content types provided out of the box by *servant*, it's a really dumb data type. @@ -464,8 +447,6 @@ And now the `MimeUnrender` class, which lets us extract values from lazy ``` haskell ignore class Accept ctype => MimeUnrender ctype a where mimeUnrender :: Proxy ctype -> ByteString -> Either String a - -- alternatively: - mimeUnrender :: Proxy ctype -> (ByteString -> Either String a) ``` We don't have much work to do there either, `Data.Aeson.eitherDecode` is @@ -496,8 +477,7 @@ HTML representation of the data they want, ready to be included in any HTML document, e.g. using [jQuery's `load` function](https://api.jquery.com/load/), simply by adding `Accept: text/html` to their request headers. -Case-studies: *servant-blaze* and *servant-lucid* -------------------------------------------------- +### Case-studies: *servant-blaze* and *servant-lucid* These days, most of the haskellers who write their HTML UIs directly from Haskell use either [blaze-html](http://hackage.haskell.org/package/blaze-html) @@ -615,8 +595,8 @@ instance ToHtml [Person] where We create some `Person` values and serve them as a list: ``` haskell -persons :: [Person] -persons = +people :: [Person] +people = [ Person "Isaac" "Newton" , Person "Albert" "Einstein" ] @@ -625,7 +605,7 @@ personAPI :: Proxy PersonAPI personAPI = Proxy server4 :: Server PersonAPI -server4 = return persons +server4 = return people app2 :: Application app2 = serve personAPI EmptyConfig server4 @@ -641,8 +621,7 @@ And we're good to go. You can run this example with `dist/build/tutorial/tutoria # or just point your browser to http://localhost:8081/persons ``` -The `ExceptT ServantErr IO` monad -================================= +## The `ExceptT ServantErr IO` monad At the heart of the handlers is the monad they run in, namely `ExceptT ServantErr IO`. One might wonder: why this monad? The answer is that it is the @@ -660,40 +639,39 @@ Let's recall some definitions. -- from the Prelude data Either e a = Left e | Right a --- from the 'either' package at --- http://hackage.haskell.org/package/either-4.3.3.2/docs/Control-Monad-Trans-Either.html -newtype ExceptT e m a - = ExceptT { runEitherT :: m (Either e a) } +-- from the 'mtl' package at +newtype ExceptT e m a = ExceptT ( m (Either e a) ) ``` In short, this means that a handler of type `ExceptT ServantErr IO a` is simply equivalent to a computation of type `IO (Either ServantErr a)`, that is, an IO action that either returns an error or a result. -The aforementioned `either` package is worth taking a look at. Perhaps most -importantly: - -``` haskell ignore -left :: Monad m => e -> ExceptT e m a -``` -Allows you to return an error from your handler (whereas `return` is enough to -return a success). +The module [`Control.Monad.Except`](https://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Except.html#t:ExceptT) +from which `ExceptT` comes is worth looking at. +Perhaps most importantly, `ExceptT` is an instance of `MonadError`, so +`throwError` can be used to return an error from your handler (whereas `return` + is enough to return a success). Most of what you'll be doing in your handlers is running some IO and, depending on the result, you might sometimes want to throw an error of some kind and abort early. The next two sections cover how to do just that. -Performing IO -------------- +### Performing IO -Another important instance from the list above is `MonadIO m => MonadIO (ExceptT e m)`. [`MonadIO`](http://hackage.haskell.org/package/transformers-0.4.3.0/docs/Control-Monad-IO-Class.html) is a class from the *transformers* package defined as: +Another important instance from the list above is `MonadIO m => MonadIO +(ExceptT e m)`. +[`MonadIO`](http://hackage.haskell.org/package/transformers-0.4.3.0/docs/Control-Monad-IO-Class.html) +is a class from the *transformers* package defined as: ``` haskell ignore class Monad m => MonadIO m where liftIO :: IO a -> m a ``` -Obviously, the `IO` monad provides a `MonadIO` instance. Hence for any type `e`, `ExceptT e IO` has a `MonadIO` instance. So if you want to run any kind of IO computation in your handlers, just use `liftIO`: +Obviously, the `IO` monad provides a `MonadIO` instance. Hence for any type +`e`, `ExceptT e IO` has a `MonadIO` instance. So if you want to run any kind of +IO computation in your handlers, just use `liftIO`: ``` haskell type IOAPI1 = "myfile.txt" :> Get '[JSON] FileContent @@ -710,8 +688,7 @@ server5 = do return (FileContent filecontent) ``` -Failing, through `ServantErr` ------------------------------ +### Failing, through `ServantErr` If you want to explicitly fail at providing the result promised by an endpoint using the appropriate HTTP status code (not found, unauthorized, etc) and some @@ -787,8 +764,7 @@ query it, first without the file and then with the file. {"content":"Hello\n"} ``` -Response headers -================ +## Response headers To add headers to your response, use [addHeader](http://hackage.haskell.org/package/servant-0.4.4/docs/Servant-API-ResponseHeaders.html). Note that this changes the type of your API, as we can see in the following example: @@ -800,9 +776,9 @@ myHandler :: Server MyHandler myHandler = return $ addHeader 1797 albert ``` +Note that the type of `addHeader x` is different than the type of `x`! -Serving static files -==================== +## Serving static files *servant-server* also provides a way to just serve the content of a directory under some path in your web API. As mentioned earlier in this document, the @@ -842,7 +818,9 @@ app3 :: Application app3 = serve codeAPI EmptyConfig server7 ``` -This server will match any request whose path starts with `/code` and will look for a file at the path described by the rest of the request path, inside the *tutorial/* directory of the path you run the program from. +This server will match any request whose path starts with `/code` and will look +for a file at the path described by the rest of the request path, inside the + *tutorial/* directory of the path you run the program from. In other words: @@ -941,8 +919,7 @@ $ curl http://localhost:8081/foo not found ``` -Nested APIs -=========== +## Nested APIs Let's see how you can define APIs in a modular way, while avoiding repetition. Consider this simple example: @@ -1130,8 +1107,7 @@ serverFor = error "..." -- or the mailing list if you get stuck! ``` -Using another monad for your handlers -===================================== +## Using another monad for your handlers Remember how `Server` turns combinators for HTTP methods into `ExceptT ServantErr IO`? Well, actually, there's more to that. `Server` is actually a simple type synonym. @@ -1143,8 +1119,7 @@ type Server api = ServerT api (ExceptT ServantErr IO) The first and main question one might have then is: how do we write handlers that run in another monad? How can we "bring back" the value from a given monad into something *servant* can understand? -Natural transformations ------------------------ +### Natural transformations If we have a function that gets us from an `m a` to an `n a`, for any `a`, what do we have? @@ -1202,8 +1177,7 @@ We unfortunately can't use `readerServerT` as an argument of `serve`, because `serve` wants a `Server ReaderAPI`, i.e., with handlers running in `ExceptT ServantErr IO`. But there's a simple solution to this. -Enter `enter` -------------- +### Enter `enter` That's right. We have just written `readerToEither`, which is exactly what we would need to apply to the results of all handlers to make the handlers have the @@ -1230,8 +1204,7 @@ $ curl http://localhost:8081/b "hi" ``` -Conclusion -========== +## Conclusion You're now equipped to write any kind of webservice/web-application using *servant*. One thing not covered here is how to incorporate your own combinators and will be the topic of a page on the website. The rest of this document focuses on *servant-client*, *servant-jquery* and *servant-docs*. diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index 9c3d664b..a39602c1 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -24,7 +24,7 @@ library build-depends: base == 4.* , base-compat , text - , aeson + , aeson >= 0.11 , blaze-html , directory , blaze-markup diff --git a/stack.yaml b/stack.yaml index feaea42b..40ddab48 100644 --- a/stack.yaml +++ b/stack.yaml @@ -18,4 +18,7 @@ extra-deps: - engine-io-wai-1.0.2 - control-monad-omega-0.3.1 - should-not-typecheck-2.0.1 +- markdown-unlit-0.4.0 +- aeson-0.11.0.0 +- fail-4.9.0.0 resolver: nightly-2015-10-08 From 0daa8048c42c7d34d4a8265d52f17e6e24f3e64b Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Thu, 18 Feb 2016 00:43:34 +0100 Subject: [PATCH 31/50] Remove stale next/previous --- doc/tutorial/Client.lhs | 5 ----- doc/tutorial/Docs.lhs | 4 ---- doc/tutorial/Javascript.lhs | 5 ----- 3 files changed, 14 deletions(-) diff --git a/doc/tutorial/Client.lhs b/doc/tutorial/Client.lhs index ea8bdbfa..60dc88e4 100644 --- a/doc/tutorial/Client.lhs +++ b/doc/tutorial/Client.lhs @@ -147,8 +147,3 @@ You can now run `dist/build/tutorial/tutorial 8` (the server) and ``` The types of the arguments for the functions are the same as for (server-side) request handlers. You now know how to use *servant-client*! - - diff --git a/doc/tutorial/Docs.lhs b/doc/tutorial/Docs.lhs index c006c0b1..adc63924 100644 --- a/doc/tutorial/Docs.lhs +++ b/doc/tutorial/Docs.lhs @@ -232,7 +232,3 @@ app = serve api EmptyConfig server ``` And if you spin up this server with `dist/build/tutorial/tutorial 10` and go to anywhere else than `/position`, `/hello` and `/marketing`, you will see the API docs in markdown. This is because `serveDocs` is attempted if the 3 other endpoints don't match and systematically succeeds since its definition is to just return some fixed bytestring with the `text/plain` content type. - - diff --git a/doc/tutorial/Javascript.lhs b/doc/tutorial/Javascript.lhs index 9ab4740e..c5f118b1 100644 --- a/doc/tutorial/Javascript.lhs +++ b/doc/tutorial/Javascript.lhs @@ -182,8 +182,3 @@ writeJSFiles = do ``` And we're good to go. Start the server with `dist/build/tutorial/tutorial 9` and go to `http://localhost:8081/`. Start typing in the name of one of the authors in our database or part of a book title, and check out how long it takes to approximate π using the method mentioned above. - - From 5625f5273e024da06789d62111244a2d976c8191 Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Thu, 18 Feb 2016 00:48:05 +0100 Subject: [PATCH 32/50] more consistent line breaks --- doc/tutorial/Server.lhs | 114 ++++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 50 deletions(-) diff --git a/doc/tutorial/Server.lhs b/doc/tutorial/Server.lhs index 79588da7..93769b54 100644 --- a/doc/tutorial/Server.lhs +++ b/doc/tutorial/Server.lhs @@ -3,29 +3,10 @@ Enough chit-chat about type-level combinators and representing an API as a type. Can we have a webservice already? -If you want to follow along with the code and run the examples while you read this guide: - -``` bash -cabal get servant-examples -cd servant-examples- -cabal sandbox init -cabal install --dependencies-only -cabal configure && cabal build -``` - -This will produce a `tutorial` executable in the -`dist/build/tutorial` directory that just runs the example corresponding -to the number specified as a command line argument: - -``` bash -$ dist/build/tutorial/tutorial -Usage: tutorial N - where N is the number of the example you want to run. -``` - ## A first example -Equipped with some basic knowledge about the way we represent API, let's now write our first webservice. +Equipped with some basic knowledge about the way we represent API, let's now +write our first webservice. The source for this tutorial section is a literate haskell file, so first we need to have some language extensions and imports: @@ -72,7 +53,13 @@ import qualified Text.Blaze.Html {-# LANGUAGE TypeFamilies #-} ``` -**Important**: the `Servant` module comes from the *servant-server* package, the one that lets us run webservers that implement a particular API type. It reexports all the types from the *servant* package that let you declare API types as well as everything you need to turn your request handlers into a fully-fledged webserver. This means that in your applications, you can just add *servant-server* as a dependency, import `Servant` and not worry about anything else. +**Important**: the `Servant` module comes from the *servant-server* package, +the one that lets us run webservers that implement a particular API type. It +reexports all the types from the *servant* package that let you declare API +types as well as everything you need to turn your request handlers into a +fully-fledged webserver. This means that in your applications, you can just add +*servant-server* as a dependency, import `Servant` and not worry about anything +else. We will write a server that will serve the following API. @@ -95,7 +82,7 @@ data User = User { name :: String , age :: Int , email :: String - , registrationDate :: Day + , registration_date :: Day } deriving (Eq, Show, Generic) instance ToJSON User @@ -140,7 +127,9 @@ server1 :: Server UserAPI1 server1 = return users1 ``` -That's it. Now we can turn `server` into an actual webserver using [wai](http://hackage.haskell.org/package/wai) and [warp](http://hackage.haskell.org/package/warp): +That's it. Now we can turn `server` into an actual webserver using +[wai](http://hackage.haskell.org/package/wai) and +[warp](http://hackage.haskell.org/package/warp): ``` haskell userAPI :: Proxy UserAPI1 @@ -179,7 +168,8 @@ $ curl http://localhost:8081/users ## More endpoints -What if we want more than one endpoint? Let's add `/albert` and `/isaac` to view the corresponding users encoded in JSON. +What if we want more than one endpoint? Let's add `/albert` and `/isaac` to +view the corresponding users encoded in JSON. ``` haskell type UserAPI2 = "users" :> Get '[JSON] [User] @@ -225,7 +215,8 @@ argument of the appropriate type automatically. You don't have to worry about manually looking up URL captures or query string parameters, or decoding/encoding data from/to JSON. Never. -We are going to use the following data types and functions to implement a server for `API`. +We are going to use the following data types and functions to implement a +server for `API`. ``` haskell type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position @@ -307,7 +298,8 @@ parameter might not always be there); - a `ReqBody contentTypeList a` becomes an argument of type `a`; -And that's it. You can see this example in action by running `dist/build/tutorial/tutorial 3`. +And that's it. You can see this example in action by running +`dist/build/tutorial/tutorial 3`. ``` bash $ curl http://localhost:8081/position/1/2 @@ -474,8 +466,8 @@ And this is all the code that lets you use `JSON` for with `ReqBody`, `Get`, `Post` and friends. We can check our understanding by implementing support for an `HTML` content type, so that users of your webservice can access an HTML representation of the data they want, ready to be included in any HTML -document, e.g. using [jQuery's `load` function](https://api.jquery.com/load/), simply by adding `Accept: -text/html` to their request headers. +document, e.g. using [jQuery's `load` function](https://api.jquery.com/load/), +simply by adding `Accept: text/html` to their request headers. ### Case-studies: *servant-blaze* and *servant-lucid* @@ -766,7 +758,8 @@ query it, first without the file and then with the file. ## Response headers -To add headers to your response, use [addHeader](http://hackage.haskell.org/package/servant-0.4.4/docs/Servant-API-ResponseHeaders.html). +To add headers to your response, use +[addHeader](http://hackage.haskell.org/package/servant/docs/Servant-API-ResponseHeaders.html). Note that this changes the type of your API, as we can see in the following example: ``` haskell @@ -824,9 +817,12 @@ for a file at the path described by the rest of the request path, inside the In other words: -- If a client requests `/code/foo.txt`, the server will look for a file at `./tutorial/foo.txt` (and fail) -- If a client requests `/code/T1.hs`, the server will look for a file at `./tutorial/T1.hs` (and succeed) -- If a client requests `/code/foo/bar/baz/movie.mp4`, the server will look for a file at `./tutorial/foo/bar/baz/movie.mp4` (and fail) +- If a client requests `/code/foo.txt`, the server will look for a file at + `./tutorial/foo.txt` (and fail) +- If a client requests `/code/T1.hs`, the server will look for a file at + `./tutorial/T1.hs` (and succeed) +- If a client requests `/code/foo/bar/baz/movie.mp4`, the server will look for + a file at `./tutorial/foo/bar/baz/movie.mp4` (and fail) Here is our little server in action. @@ -921,7 +917,8 @@ not found ## Nested APIs -Let's see how you can define APIs in a modular way, while avoiding repetition. Consider this simple example: +Let's see how you can define APIs in a modular way, while avoiding repetition. +Consider this simple example: ``` haskell type UserAPI3 = -- view the user with given userid, in JSON @@ -940,7 +937,8 @@ type UserAPI4 = Capture "userid" Int :> ) ``` -However, you have to be aware that this has an effect on the type of the corresponding `Server`: +However, you have to be aware that this has an effect on the type of the +corresponding `Server`: ``` haskell ignore Server UserAPI3 = (Int -> ExceptT ServantErr IO User) @@ -952,7 +950,8 @@ Server UserAPI4 = Int -> ( ExceptT ServantErr IO User ``` In the first case, each handler receives the *userid* argument. In the latter, -the whole `Server` takes the *userid* and has handlers that are just computations in `ExceptT`, with no arguments. In other words: +the whole `Server` takes the *userid* and has handlers that are just +computations in `ExceptT`, with no arguments. In other words: ``` haskell server8 :: Server UserAPI3 @@ -977,7 +976,10 @@ server9 userid = getUser userid :<|> deleteUser userid deleteUser = error "..." ``` -Note that there's nothing special about `Capture` that lets you "factor it out": this can be done with any combinator. Here are a few examples of APIs with a combinator factored out for which we can write a perfectly valid `Server`. +Note that there's nothing special about `Capture` that lets you "factor it +out": this can be done with any combinator. Here are a few examples of APIs +with a combinator factored out for which we can write a perfectly valid +`Server`. ``` haskell -- we just factor out the "users" path fragment @@ -1002,7 +1004,8 @@ newtype Token = Token ByteString newtype SecretData = SecretData ByteString ``` -This approach lets you define APIs modularly and assemble them all into one big API type only at the end. +This approach lets you define APIs modularly and assemble them all into one big +API type only at the end. ``` haskell type UsersAPI = @@ -1080,7 +1083,8 @@ server10 :: Server CombinedAPI server10 = usersServer :<|> productsServer ``` -Finally, we can realize the user and product APIs are quite similar and abstract that away: +Finally, we can realize the user and product APIs are quite similar and +abstract that away: ``` haskell -- API for values of type 'a' @@ -1109,15 +1113,25 @@ serverFor = error "..." ## Using another monad for your handlers -Remember how `Server` turns combinators for HTTP methods into `ExceptT ServantErr IO`? Well, actually, there's more to that. `Server` is actually a simple type synonym. +Remember how `Server` turns combinators for HTTP methods into `ExceptT +ServantErr IO`? Well, actually, there's more to that. `Server` is actually a +simple type synonym. ``` haskell ignore type Server api = ServerT api (ExceptT ServantErr IO) ``` -`ServerT` is the actual type family that computes the required types for the handlers that's part of the `HasServer` class. It's like `Server` except that it takes a third parameter which is the monad you want your handlers to run in, or more generally the return types of your handlers. This third parameter is used for specifying the return type of the handler for an endpoint, e.g when computing `ServerT (Get '[JSON] Person) SomeMonad`. The result would be `SomeMonad Person`. +`ServerT` is the actual type family that computes the required types for the +handlers that's part of the `HasServer` class. It's like `Server` except that +it takes a third parameter which is the monad you want your handlers to run in, +or more generally the return types of your handlers. This third parameter is +used for specifying the return type of the handler for an endpoint, e.g when +computing `ServerT (Get '[JSON] Person) SomeMonad`. The result would be +`SomeMonad Person`. -The first and main question one might have then is: how do we write handlers that run in another monad? How can we "bring back" the value from a given monad into something *servant* can understand? +The first and main question one might have then is: how do we write handlers +that run in another monad? How can we "bring back" the value from a given monad +into something *servant* can understand? ### Natural transformations @@ -1185,7 +1199,8 @@ right type for `serve`. Being cumbersome to do by hand, we provide a function `enter` which takes a natural transformation between two parametrized types `m` and `n` and a `ServerT someapi m`, and returns a `ServerT someapi n`. -In our case, we can wrap up our little webservice by using `enter readerToEither` on our handlers. +In our case, we can wrap up our little webservice by using `enter +readerToEither` on our handlers. ``` haskell readerServer :: Server ReaderAPI @@ -1195,7 +1210,8 @@ app4 :: Application app4 = serve readerAPI EmptyConfig readerServer ``` -And we can indeed see this webservice in action by running `dist/build/tutorial/tutorial 7`. +And we can indeed see this webservice in action by running +`dist/build/tutorial/tutorial 7`. ``` bash $ curl http://localhost:8081/a @@ -1206,9 +1222,7 @@ $ curl http://localhost:8081/b ## Conclusion -You're now equipped to write any kind of webservice/web-application using *servant*. One thing not covered here is how to incorporate your own combinators and will be the topic of a page on the website. The rest of this document focuses on *servant-client*, *servant-jquery* and *servant-docs*. - - +You're now equipped to write any kind of webservice/web-application using +*servant*. One thing not covered here is how to incorporate your own +combinators and will be the topic of a page on the website. The rest of this +document focuses on *servant-client*, *servant-jquery* and *servant-docs*. From 9263f9790f1d4bf98c637c64af79501e2c5606c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 18 Feb 2016 17:02:11 +0100 Subject: [PATCH 33/50] tutorial: restructuring --- doc/CONTRIBUTING.md | 1 - doc/README.md | 1 - doc/index.rst | 14 +++++----- doc/introduction.rst | 44 ++++++++++++++++++++++++++++++++ doc/links.rst | 34 +++++++++++++++++++++++++ doc/tutorial/index.rst | 58 ++---------------------------------------- 6 files changed, 88 insertions(+), 64 deletions(-) delete mode 120000 doc/CONTRIBUTING.md delete mode 120000 doc/README.md create mode 100644 doc/introduction.rst create mode 100644 doc/links.rst diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md deleted file mode 120000 index 44fcc634..00000000 --- a/doc/CONTRIBUTING.md +++ /dev/null @@ -1 +0,0 @@ -../CONTRIBUTING.md \ No newline at end of file diff --git a/doc/README.md b/doc/README.md deleted file mode 120000 index 32d46ee8..00000000 --- a/doc/README.md +++ /dev/null @@ -1 +0,0 @@ -../README.md \ No newline at end of file diff --git a/doc/index.rst b/doc/index.rst index ca7b5e5f..9757ec1b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,12 +1,14 @@ -servant – Type-Level Web DSL -============================ +servant – A Type-Level Web DSL +============================== + +.. image:: https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png Documentation table of contents ------------------------------- .. toctree:: - :maxdepth: 2 + :maxdepth: 2 - README.md - tutorial/index.rst - CONTRIBUTING.md + introduction.rst + tutorial/index.rst + links.rst diff --git a/doc/introduction.rst b/doc/introduction.rst new file mode 100644 index 00000000..6c5050bc --- /dev/null +++ b/doc/introduction.rst @@ -0,0 +1,44 @@ +Introduction +------------ + +*servant* has the following guiding principles: + +- concision + + This is a pretty wide-ranging principle. You should be able to get nice + documentation for your web servers, and client libraries, without repeating + yourself. You should not have to manually serialize and deserialize your + resources, but only declare how to do those things *once per type*. If a + bunch of your handlers take the same query parameters, you shouldn't have to + repeat that logic for each handler, but instead just "apply" it to all of + them at once. Your handlers shouldn't be where composition goes to die. And + so on. + +- flexibility + + If we haven't thought of your use case, it should still be easily + achievable. If you want to use templating library X, go ahead. Forms? Do + them however you want, but without difficulty. We're not opinionated. + +- separation of concerns + + Your handlers and your HTTP logic should be separate. True to the philosphy + at the core of HTTP and REST, with *servant* your handlers return normal + Haskell datatypes - that's the resource. And then from a description of your + API, *servant* handles the *presentation* (i.e., the Content-Types). But + that's just one example. + +- type safety + + Want to be sure your API meets a specification? Your compiler can check + that for you. Links you can be sure exist? You got it. + +To stick true to these principles, we do things a little differently than you +might expect. The core idea is *reifying the description of your API*. Once +reified, everything follows. We think we might be the first web framework to +reify API descriptions in an extensible way. We're pretty sure we're the first +to reify it as *types*. + +To be able to write a webservice you only need to read the first two sections, +but the goal of this document being to get you started with servant, we also +cover the couple of ways you can extend servant for a great good. diff --git a/doc/links.rst b/doc/links.rst new file mode 100644 index 00000000..8f28d16f --- /dev/null +++ b/doc/links.rst @@ -0,0 +1,34 @@ + +Helpful Links +------------- + +- the central documentation (this site): + `haskell-servant.readthedocs.org `_ + +- the github repo: + `github.com/haskell-servant/servant `_ + +- the issue tracker (Feel free to create issues and submit PRs!): + `https://github.com/haskell-servant/servant/issues `_ + +- the irc channel: + #servant on freenode + +- the mailing list: + `groups.google.com/forum/#!forum/haskell-servant `_ + +- blog posts and videos and slides of some talks on servant: + `haskell-servant.github.io `_ + +- the servant packages on hackage: + + - `hackage.haskell.org/package/servant `_ + - `hackage.haskell.org/package/servant-server `_ + - `hackage.haskell.org/package/servant-client `_ + - `hackage.haskell.org/package/servant-blaze `_ + - `hackage.haskell.org/package/servant-lucid `_ + - `hackage.haskell.org/package/servant-cassava `_ + - `hackage.haskell.org/package/servant-docs `_ + - `hackage.haskell.org/package/servant-foreign `_ + - `hackage.haskell.org/package/servant-js `_ + - `hackage.haskell.org/package/servant-mock `_ diff --git a/doc/tutorial/index.rst b/doc/tutorial/index.rst index ab212368..37dab25f 100644 --- a/doc/tutorial/index.rst +++ b/doc/tutorial/index.rst @@ -1,64 +1,10 @@ -Servant tutorial -================ +Tutorial +======== This is an introductory tutorial to the current version of *servant*, which is **0.4**. Any comment or issue can be directed to `this website's issue tracker `_. -Github -------- - -- the servant packages: `haskell-servant/servant `_ -- the website (including this tutorial): `haskell-servant/haskell-servant.github.io `_ -- Feel free to use the issue tracker (or to send PRs!) on the website's repository to give feedback and suggestions about this tutorial - -Introduction -------------- - -*servant* has the following guiding principles: - -- concision - - This is a pretty wide-ranging principle. You should be able to get nice - documentation for your web servers, and client libraries, without repeating - yourself. You should not have to manually serialize and deserialize your - resources, but only declare how to do those things *once per type*. If a - bunch of your handlers take the same query parameters, you shouldn't have to - repeat that logic for each handler, but instead just "apply" it to all of - them at once. Your handlers shouldn't be where composition goes to die. And - so on. - -- flexibility - - If we haven't thought of your use case, it should still be easily - achievable. If you want to use templating library X, go ahead. Forms? Do - them however you want, but without difficulty. We're not opinionated. - -- separation of concerns - - Your handlers and your HTTP logic should be separate. True to the philosphy - at the core of HTTP and REST, with *servant* your handlers return normal - Haskell datatypes - that's the resource. And then from a description of your - API, *servant* handles the *presentation* (i.e., the Content-Types). But - that's just one example. - -- type safety - - Want to be sure your API meets a specification? Your compiler can check - that for you. Links you can be sure exist? You got it. - -To stick true to these principles, we do things a little differently than you -might expect. The core idea is *reifying the description of your API*. Once -reified, everything follows. We think we might be the first web framework to -reify API descriptions in an extensible way. We're pretty sure we're the first -to reify it as *types*. - -To be able to write a webservice you only need to read the first two sections, -but the goal of this document being to get you started with servant, we also -cover the couple of ways you can extend servant for a great good. - -Tutorial ---------- .. toctree:: :maxdepth: 1 From 23d6671c6c8705c2fe40ea4b17ee9a30b5bd326e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 18 Feb 2016 18:13:43 +0100 Subject: [PATCH 34/50] tutorial: tweak some titles --- doc/index.rst | 11 +++++++++-- doc/links.rst | 2 +- doc/tutorial/Client.lhs | 2 +- doc/tutorial/Docs.lhs | 2 +- doc/tutorial/Javascript.lhs | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 9757ec1b..b1e24c99 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -3,8 +3,15 @@ servant – A Type-Level Web DSL .. image:: https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png -Documentation table of contents -------------------------------- +``servant`` is a set of packages for writing web applications and tools around them. +It allows to + +- write servers, +- obtain clients (in haskell), +- generate client functions for other programming languages and +- generate documentation for your web applications. + +All in a type-safe manner. .. toctree:: :maxdepth: 2 diff --git a/doc/links.rst b/doc/links.rst index 8f28d16f..5f14c527 100644 --- a/doc/links.rst +++ b/doc/links.rst @@ -12,7 +12,7 @@ Helpful Links `https://github.com/haskell-servant/servant/issues `_ - the irc channel: - #servant on freenode + ``#servant`` on freenode - the mailing list: `groups.google.com/forum/#!forum/haskell-servant `_ diff --git a/doc/tutorial/Client.lhs b/doc/tutorial/Client.lhs index 60dc88e4..201f187d 100644 --- a/doc/tutorial/Client.lhs +++ b/doc/tutorial/Client.lhs @@ -1,4 +1,4 @@ -# Deriving Haskell functions to query an API +# Querying an API While defining handlers that serve an API has a lot to it, querying an API is simpler: we do not care about what happens inside the webserver, we just need to know how to talk to it and get a response back. Except that we usually have to write the querying functions by hand because the structure of the API isn't a first class citizen and can't be inspected to generate a bunch of client-side functions. diff --git a/doc/tutorial/Docs.lhs b/doc/tutorial/Docs.lhs index adc63924..b1edc38e 100644 --- a/doc/tutorial/Docs.lhs +++ b/doc/tutorial/Docs.lhs @@ -1,4 +1,4 @@ -# Generating documentation from API types +# Documenting an API The source for this tutorial section is a literate haskell file, so first we need to have some language extensions and imports: diff --git a/doc/tutorial/Javascript.lhs b/doc/tutorial/Javascript.lhs index c5f118b1..85d16e20 100644 --- a/doc/tutorial/Javascript.lhs +++ b/doc/tutorial/Javascript.lhs @@ -1,4 +1,4 @@ -# Deriving Javascript functions to query an API +# Generating Javascript functions to query an API We will now see how *servant* lets you turn an API type into javascript functions that you can call to query a webservice. The derived code assumes you From 71a21403a033a6cefde6009e76fc6321543509ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 18 Feb 2016 18:17:56 +0100 Subject: [PATCH 35/50] tutorial: tweak index page --- doc/index.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index b1e24c99..2f300a1f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -3,11 +3,11 @@ servant – A Type-Level Web DSL .. image:: https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png -``servant`` is a set of packages for writing web applications and tools around them. -It allows to +``servant`` is a set of packages for declaring web APIs at the type-level and +then using those API specifications to: -- write servers, -- obtain clients (in haskell), +- write servers (this part of ``servant`` can be considered a web framework), +- obtain client functions (in haskell), - generate client functions for other programming languages and - generate documentation for your web applications. From 140da7a7b0311abd079f278239ba77a99a5bd94e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Thu, 18 Feb 2016 22:12:27 +0100 Subject: [PATCH 36/50] tutorial: tweaks --- doc/index.rst | 4 ++-- doc/introduction.rst | 10 +++------- doc/tutorial/index.rst | 8 +++++--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 2f300a1f..2c44df0a 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -3,10 +3,10 @@ servant – A Type-Level Web DSL .. image:: https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png -``servant`` is a set of packages for declaring web APIs at the type-level and +**servant** is a set of packages for declaring web APIs at the type-level and then using those API specifications to: -- write servers (this part of ``servant`` can be considered a web framework), +- write servers (this part of **servant** can be considered a web framework), - obtain client functions (in haskell), - generate client functions for other programming languages and - generate documentation for your web applications. diff --git a/doc/introduction.rst b/doc/introduction.rst index 6c5050bc..77ef306b 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -1,7 +1,7 @@ Introduction ------------ -*servant* has the following guiding principles: +**servant** has the following guiding principles: - concision @@ -23,9 +23,9 @@ Introduction - separation of concerns Your handlers and your HTTP logic should be separate. True to the philosphy - at the core of HTTP and REST, with *servant* your handlers return normal + at the core of HTTP and REST, with **servant** your handlers return normal Haskell datatypes - that's the resource. And then from a description of your - API, *servant* handles the *presentation* (i.e., the Content-Types). But + API, **servant** handles the *presentation* (i.e., the Content-Types). But that's just one example. - type safety @@ -38,7 +38,3 @@ might expect. The core idea is *reifying the description of your API*. Once reified, everything follows. We think we might be the first web framework to reify API descriptions in an extensible way. We're pretty sure we're the first to reify it as *types*. - -To be able to write a webservice you only need to read the first two sections, -but the goal of this document being to get you started with servant, we also -cover the couple of ways you can extend servant for a great good. diff --git a/doc/tutorial/index.rst b/doc/tutorial/index.rst index 37dab25f..cf2cfd8e 100644 --- a/doc/tutorial/index.rst +++ b/doc/tutorial/index.rst @@ -1,9 +1,11 @@ Tutorial ======== -This is an introductory tutorial to the current version of *servant*, which is -**0.4**. Any comment or issue can be directed to `this website's issue -tracker `_. +This is an introductory tutorial to **servant**. + +(Any comments, issues or feedback about the tutorial can be handled +through +`servant's issue tracker `_.) .. toctree:: From 434c163aa1cf934b037189b96cd3290207797492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Tue, 23 Feb 2016 13:42:48 +0100 Subject: [PATCH 37/50] tutorial: read through ApiType.lhs --- doc/index.rst | 5 +-- doc/tutorial/ApiType.lhs | 72 ++++++++++++++++++---------------------- 2 files changed, 35 insertions(+), 42 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 2c44df0a..eebba2dd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -8,8 +8,9 @@ then using those API specifications to: - write servers (this part of **servant** can be considered a web framework), - obtain client functions (in haskell), -- generate client functions for other programming languages and -- generate documentation for your web applications. +- generate client functions for other programming languages, +- generate documentation for your web applications +- and more... All in a type-safe manner. diff --git a/doc/tutorial/ApiType.lhs b/doc/tutorial/ApiType.lhs index bbe1da43..ff64a0ae 100644 --- a/doc/tutorial/ApiType.lhs +++ b/doc/tutorial/ApiType.lhs @@ -24,8 +24,8 @@ You *should* be able to formalize that. And then use the formalized version to get you much of the way towards writing a web app. And all the way towards getting some client libraries, and documentation, and more. -How would we describe it with servant? As mentioned earlier, an endpoint -description is a good old Haskell **type**: +How would we describe it with **servant**? An endpoint description is a good old +Haskell **type**: ``` haskell type UserAPI = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User] @@ -66,15 +66,14 @@ type UserAPI2 = "users" :> "list-all" :> Get '[JSON] [User] :<|> "list-all" :> "users" :> Get '[JSON] [User] ``` -*servant* provides a fair amount of combinators out-of-the-box, but you can -always write your own when you need it. Here's a quick overview of all the -combinators that servant comes with. +**servant** provides a fair amount of combinators out-of-the-box, but you can +always write your own when you need it. Here's a quick overview of the most +often needed the combinators that **servant** comes with. ## Combinators ### Static strings - As you've already seen, you can use type-level strings (enabled with the `DataKinds` language extension) for static path fragments. Chaining them amounts to `/`-separating them in a URL. @@ -87,7 +86,6 @@ type UserAPI3 = "users" :> "list-all" :> "now" :> Get '[JSON] [User] ### `Delete`, `Get`, `Patch`, `Post` and `Put` - The `Get` combinator is defined in terms of the more general `Verb`: ``` haskell ignore data Verb method (statusCode :: Nat) (contentType :: [*]) a @@ -120,15 +118,14 @@ type UserAPI4 = "users" :> Get '[JSON] [User] ### `Capture` - -URL captures are parts of the URL that are variable and whose actual value is +URL captures are segments of the path of a URL that are variable and whose actual value is captured and passed to the request handlers. In many web frameworks, you'll see it written as in `/users/:userid`, with that leading `:` denoting that `userid` is just some kind of variable name or placeholder. For instance, if `userid` is supposed to range over all integers greater or equal to 1, our endpoint will match requests made to `/users/1`, `/users/143` and so on. -The `Capture` combinator in servant takes a (type-level) string representing +The `Capture` combinator in **servant** takes a (type-level) string representing the "name of the variable" and a type, which indicates the type we want to decode the "captured value" to. @@ -155,17 +152,16 @@ type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User In the second case, `DeleteNoContent` specifies a 204 response code, `JSON` specifies the content types on which the handler will match, -and `NoContent` is a Haskell type isomorphic to `()` used to represent -a trivial piece of information. +and `NoContent` says that the response will always be empty. ### `QueryParam`, `QueryParams`, `QueryFlag` -`QueryParam`, `QueryParams` and `QueryFlag` are about query string -parameters, i.e., those parameters that come after the question mark +`QueryParam`, `QueryParams` and `QueryFlag` are about parameters in the query string, +i.e., those parameters that come after the question mark (`?`) in URLs, like `sortby` in `/users?sortby=age`, whose value is set to `age`. `QueryParams` lets you specify that the query parameter is actually a list of values, which can be specified using -`?param[]=value1¶m[]=value2`. This represents a list of values +`?param=value1¶m=value2`. This represents a list of values composed of `value1` and `value2`. `QueryFlag` lets you specify a boolean-like query parameter where a client isn't forced to specify a value. The absence or presence of the parameter's name in the query @@ -190,7 +186,7 @@ type UserAPI6 = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User] ``` Again, your handlers don't have to deserialize these things (into, for example, -a `SortBy`). *servant* takes care of it. +a `SortBy`). **servant** takes care of it. ### `ReqBody` @@ -201,9 +197,9 @@ users: instead of passing each field of the user as a separate query string parameter or something dirty like that, we can group all the data into a JSON object. This has the advantage of supporting nested objects. -*servant*'s `ReqBody` combinator takes a list of content types in which the +**servant**'s `ReqBody` combinator takes a list of content types in which the data encoded in the request body can be represented and the type of that data. -And, as you might have guessed, you don't have to check the content-type +And, as you might have guessed, you don't have to check the content type header, and do the deserialization yourself. We do it for you. And return `Bad Request` or `Unsupported Content Type` as appropriate. @@ -231,12 +227,11 @@ type UserAPI7 = "users" :> ReqBody '[JSON] User :> Post '[JSON] User ### Request `Header`s - Request headers are used for various purposes, from caching to carrying auth-related data. They consist of a header name and an associated value. An example would be `Accept: application/json`. -The `Header` combinator in servant takes a type-level string for the header +The `Header` combinator in **servant** takes a type-level string for the header name and the type to which we want to decode the header's value (from some textual representation), as illustrated below: @@ -255,10 +250,10 @@ type UserAPI8 = "users" :> Header "User-Agent" Text :> Get '[JSON] [User] ### Content types So far, whenever we have used a combinator that carries a list of content -types, we've always specified `'[JSON]`. However, *servant* lets you use several +types, we've always specified `'[JSON]`. However, **servant** lets you use several content types, and also lets you define your own content types. -Four content-types are provided out-of-the-box by the core *servant* package: +Four content types are provided out-of-the-box by the core **servant** package: `JSON`, `PlainText`, `FormUrlEncoded` and `OctetStream`. If for some obscure reason you wanted one of your endpoints to make your user data available under those 4 formats, you would write the API type as below: @@ -267,18 +262,18 @@ those 4 formats, you would write the API type as below: type UserAPI9 = "users" :> Get '[JSON, PlainText, FormUrlEncoded, OctetStream] [User] ``` -We also provide an HTML content-type, but since there's no single library -that everyone uses, we decided to release 2 packages, *servant-lucid* and -*servant-blaze*, to provide HTML encoding of your data. +(There are other packages that provide other content types. For example +**servant-lucid** and **servant-blaze** allow to generate html pages (using +**lucid** and **blaze-html**) and both come with a content type for html.) We will further explain how these content types and your data types can play -together in the [section about serving an API](/tutorial/server.html). +together in the [section about serving an API](Server.html). ### Response `Headers` Just like an HTTP request, the response generated by a webserver can carry -headers too. *servant* provides a `Headers` combinator that carries a list of -`Header` and can be used by simply wrapping the "return type" of an endpoint +headers too. **servant** provides a `Headers` combinator that carries a list of +`Header` types and can be used by simply wrapping the "return type" of an endpoint with it. ``` haskell ignore @@ -292,11 +287,12 @@ response, you could write it as below: type UserAPI10 = "users" :> Get '[JSON] (Headers '[Header "User-Count" Integer] [User]) ``` -### Interoperability with other WAI `Application`s: `Raw` +### Interoperability with `wai`: `Raw` -Finally, we also include a combinator named `Raw` that can be used for two reasons: - -- You want to serve static files from a given directory. In that case you can just say: +Finally, we also include a combinator named `Raw` that provides an escape hatch +to the underlying low-level web library `wai`. It can be used when +you want to plug a [wai `Application`](http://hackage.haskell.org/package/wai) +into your webservice: ``` haskell type UserAPI11 = "users" :> Get '[JSON] [User] @@ -309,11 +305,7 @@ type UserAPI11 = "users" :> Get '[JSON] [User] -- at the right path ``` -- You more generally want to plug a [WAI `Application`](http://hackage.haskell.org/package/wai) -into your webservice. Static file serving is a specific example of that. The API type would look the -same as above though. (You can even combine *servant* with other web frameworks -this way!) - - +One example for this is if you want to serve a directory of static files along +with the rest of your API. But you can plug in everything that is an +`Application`, e.g. a whole web application written in any of the web +frameworks that support `wai`. From e68cf28750b390c79fd3c22d8a38ab89a333a84b Mon Sep 17 00:00:00 2001 From: rwobben Date: Fri, 26 Feb 2016 10:13:22 +0000 Subject: [PATCH 38/50] change a dependency and delete EmptyConfig --- doc/tutorial/Server.lhs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/tutorial/Server.lhs b/doc/tutorial/Server.lhs index 93769b54..d90c2c06 100644 --- a/doc/tutorial/Server.lhs +++ b/doc/tutorial/Server.lhs @@ -29,7 +29,7 @@ import Prelude.Compat import Control.Monad.IO.Class import Control.Monad.Reader import Control.Monad.Trans.Except -import Data.Aeson +import Data.Aeson.Compat import Data.Aeson.Types import Data.Attoparsec.ByteString import Data.ByteString (ByteString) @@ -139,7 +139,7 @@ userAPI = Proxy -- which you can think of as an "abstract" web application, -- not yet a webserver. app1 :: Application -app1 = serve userAPI EmptyConfig server1 +app1 = serve userAPI server1 ``` The `userAPI` bit is, alas, boilerplate (we need it to guide type inference). From e84fea334a10c27c38d14d17525d00e7eb8bb429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Sat, 27 Feb 2016 19:53:03 +0100 Subject: [PATCH 39/50] tutorial: read through Server.lhs --- doc/tutorial/Docs.lhs | 2 +- doc/tutorial/Javascript.lhs | 2 +- doc/tutorial/Server.lhs | 326 ++++++++++++------------------------ 3 files changed, 109 insertions(+), 221 deletions(-) diff --git a/doc/tutorial/Docs.lhs b/doc/tutorial/Docs.lhs index b1edc38e..dbd3233d 100644 --- a/doc/tutorial/Docs.lhs +++ b/doc/tutorial/Docs.lhs @@ -228,7 +228,7 @@ server = Server.server3 :<|> serveDocs plain = ("Content-Type", "text/plain") app :: Application -app = serve api EmptyConfig server +app = serve api server ``` And if you spin up this server with `dist/build/tutorial/tutorial 10` and go to anywhere else than `/position`, `/hello` and `/marketing`, you will see the API docs in markdown. This is because `serveDocs` is attempted if the 3 other endpoints don't match and systematically succeeds since its definition is to just return some fixed bytestring with the `text/plain` content type. diff --git a/doc/tutorial/Javascript.lhs b/doc/tutorial/Javascript.lhs index 85d16e20..600c8327 100644 --- a/doc/tutorial/Javascript.lhs +++ b/doc/tutorial/Javascript.lhs @@ -134,7 +134,7 @@ server' = server :<|> serveDirectory "tutorial/t9" app :: Application -app = serve api' EmptyConfig server' +app = serve api' server' ``` Why two different API types, proxies and servers though? Simply because we don't want to generate javascript functions for the `Raw` part of our API type, so we need a `Proxy` for our API type `API'` without its `Raw` endpoint. diff --git a/doc/tutorial/Server.lhs b/doc/tutorial/Server.lhs index d90c2c06..ab29b59e 100644 --- a/doc/tutorial/Server.lhs +++ b/doc/tutorial/Server.lhs @@ -5,7 +5,7 @@ type. Can we have a webservice already? ## A first example -Equipped with some basic knowledge about the way we represent API, let's now +Equipped with some basic knowledge about the way we represent APIs, let's now write our first webservice. The source for this tutorial section is a literate haskell file, so first we @@ -26,7 +26,7 @@ module Server where import Prelude () import Prelude.Compat -import Control.Monad.IO.Class +import Control.Monad.Except import Control.Monad.Reader import Control.Monad.Trans.Except import Data.Aeson.Compat @@ -34,6 +34,7 @@ import Data.Aeson.Types import Data.Attoparsec.ByteString import Data.ByteString (ByteString) import Data.List +import Data.Maybe import Data.String.Conversions import Data.Time.Calendar import GHC.Generics @@ -49,16 +50,12 @@ import qualified Data.Aeson.Parser import qualified Text.Blaze.Html ``` -``` haskell ignore -{-# LANGUAGE TypeFamilies #-} -``` - -**Important**: the `Servant` module comes from the *servant-server* package, +**Important**: the `Servant` module comes from the **servant-server** package, the one that lets us run webservers that implement a particular API type. It -reexports all the types from the *servant* package that let you declare API +reexports all the types from the **servant** package that let you declare API types as well as everything you need to turn your request handlers into a fully-fledged webserver. This means that in your applications, you can just add -*servant-server* as a dependency, import `Servant` and not worry about anything +**servant-server** as a dependency, import `Servant` and not worry about anything else. We will write a server that will serve the following API. @@ -154,9 +151,8 @@ main = run 8081 app1 You can put this all into a file or just grab [servant's repo](http://github.com/haskell-servant/servant) and look at the -*servant-examples* directory. The code we have just explored is in -*tutorial/T1.hs*, runnable with -`dist/build/tutorial/tutorial 1`. +*doc/tutorial* directory. This code (the source of this web page) is in +*doc/tutorial/Server.lhs*. If you run it, you can go to `http://localhost:8081/users` in your browser or query it with curl and you see: @@ -192,7 +188,7 @@ users2 = [isaac, albert] Now, just like we separate the various endpoints in `UserAPI` with `:<|>`, we are going to separate the handlers with `:<|>` too! They must be provided in -the same order as the one they appear in in the API type. +the same order as in in the API type. ``` haskell server2 :: Server UserAPI2 @@ -201,9 +197,8 @@ server2 = return users2 :<|> return isaac ``` -And that's it! You can run this example with -`dist/build/tutorial/tutorial 2` and check out the data available -at `/users`, `/albert` and `/isaac`. +And that's it! You can run this example in the same way that we showed for +`server1` and check out the data available at `/users`, `/albert` and `/isaac`. ## From combinators to handler arguments @@ -298,8 +293,7 @@ parameter might not always be there); - a `ReqBody contentTypeList a` becomes an argument of type `a`; -And that's it. You can see this example in action by running -`dist/build/tutorial/tutorial 3`. +And that's it. Here's the example in action: ``` bash $ curl http://localhost:8081/position/1/2 @@ -312,19 +306,18 @@ $ curl -X POST -d '{"name":"Alp Mestanogullari", "email" : "alp@foo.com", "age": {"subject":"Hey Alp Mestanogullari, we miss you!","body":"Hi Alp Mestanogullari,\n\nSince you've recently turned 25, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"} ``` -For reference, here's a list of some combinators from *servant* and for those -that get turned into arguments to the handlers, the type of the argument. +For reference, here's a list of some combinators from **servant**: > - `Delete`, `Get`, `Patch`, `Post`, `Put`: these do not become arguments. They provide the return type of handlers, which usually is `ExceptT ServantErr IO `. > - `Capture "something" a` becomes an argument of type `a`. - > - `QueryParam "something" a`, `MatrixParam "something" a`, `Header "something" a` all become arguments of type `Maybe a`, because there might be no value at all specified by the client for these. - > - `QueryFlag "something"` and `MatrixFlag "something"` get turned into arguments of type `Bool`. - > - `QueryParams "something" a` and `MatrixParams "something" a` get turned into arguments of type `[a]`. + > - `QueryParam "something" a`, `Header "something" a` all become arguments of type `Maybe a`, because there might be no value at all specified by the client for these. + > - `QueryFlag "something"` gets turned into an argument of type `Bool`. + > - `QueryParams "something" a` gets turned into an argument of type `[a]`. > - `ReqBody contentTypes a` gets turned into an argument of type `a`. ## The `FromHttpApiData`/`ToHttpApiData` classes -Wait... How does *servant* know how to decode the `Int`s from the URL? Or how +Wait... How does **servant** know how to decode the `Int`s from the URL? Or how to decode a `ClientInfo` value from the request body? This is what this and the following two sections address. @@ -333,7 +326,7 @@ following two sections address. corresponding (textual) value in the request's "metadata". How types are decoded from headers, captures, and query params is expressed in a class `FromHttpApiData` (from the package -[*http-api-data*](http://hackage.haskell.org/package/http-api-data)): +[**http-api-data**](http://hackage.haskell.org/package/http-api-data)): ``` haskell ignore class FromHttpApiData a where @@ -355,15 +348,15 @@ As you can see, as long as you provide either `parseUrlPiece` (for `Capture`s) or `parseQueryParam` (for `QueryParam`s), the other methods will be defined in terms of this. -*http-api-data* provides a decent number of instances, helpers for defining new +**http-api-data** provides a decent number of instances, helpers for defining new ones, and wonderful documentation. There's not much else to say about these classes. You will need instances for them when using `Capture`, `QueryParam`, `QueryParams`, and `Header` with your types. You will need `FromHttpApiData` instances for server-side request handlers and `ToHttpApiData` instances only when using -*servant-client*, as described in the [section about deriving haskell -functions to query an API](/tutorial/client.html). +**servant-client**, as described in the [section about deriving haskell +functions to query an API](Client.html). ## Using content-types with your data types @@ -371,14 +364,15 @@ The same principle was operating when decoding request bodies from JSON, and responses *into* JSON. (JSON is just the running example - you can do this with any content-type.) -This section introduces a couple of typeclasses provided by *servant* that make +This section introduces a couple of typeclasses provided by **servant** that make all of this work. ### The truth behind `JSON` -What exactly is `JSON`? Like the 3 other content types provided out of the box -by *servant*, it's a really dumb data type. +What exactly is `JSON` (the type as used in `Get '[JSON] User`)? Like the 3 +other content-types provided out of the box by **servant**, it's a really dumb +data type. ``` haskell ignore data JSON @@ -388,14 +382,15 @@ data OctetStream ``` Obviously, this is not all there is to `JSON`, otherwise it would be quite -pointless. Like most of the data types in *servant*, `JSON` is mostly there as +pointless. Like most of the data types in **servant**, `JSON` is mostly there as a special *symbol* that's associated with encoding (resp. decoding) to (resp. from) the *JSON* format. The way this association is performed can be decomposed into two steps. The first step is to provide a proper -[`MediaType`](https://hackage.haskell.org/package/http-media-0.6.2/docs/Network-HTTP-Media.html) -representation for `JSON`, or for your own content types. If you look at the +`MediaType` (from +[**http-media**](https://hackage.haskell.org/package/http-media-0.6.2/docs/Network-HTTP-Media.html)) +representation for `JSON`, or for your own content-types. If you look at the haddocks from this link, you can see that we just have to specify `application/json` using the appropriate functions. In our case, we can just use `(//) :: ByteString -> ByteString -> MediaType`. The precise way to specify @@ -411,14 +406,14 @@ instance Accept JSON where ``` The second step is centered around the `MimeRender` and `MimeUnrender` classes. -These classes just let you specify a way to respectively encode and decode -values respectively into or from your content-type's representation. +These classes just let you specify a way to encode and decode +values into or from your content-type's representation. ``` haskell ignore class Accept ctype => MimeRender ctype a where - mimeRender :: Proxy ctype -> a -> ByteString + mimeRender :: Proxy ctype -> a -> ByteString -- alternatively readable as: - mimeRender :: Proxy ctype -> (a -> ByteString) + mimeRender :: Proxy ctype -> (a -> ByteString) ``` Given a content-type and some user type, `MimeRender` provides a function that @@ -444,7 +439,7 @@ class Accept ctype => MimeUnrender ctype a where We don't have much work to do there either, `Data.Aeson.eitherDecode` is precisely what we need. However, it only allows arrays and objects as toplevel JSON values and this has proven to get in our way more than help us so we wrote -our own little function around *aeson* and *attoparsec* that allows any type of +our own little function around **aeson** and **attoparsec** that allows any type of JSON value at the toplevel of a "JSON document". Here's the definition in case you are curious. @@ -462,20 +457,20 @@ instance FromJSON a => MimeUnrender JSON a where mimeUnrender _ = eitherDecodeLenient ``` -And this is all the code that lets you use `JSON` for with `ReqBody`, `Get`, +And this is all the code that lets you use `JSON` with `ReqBody`, `Get`, `Post` and friends. We can check our understanding by implementing support -for an `HTML` content type, so that users of your webservice can access an +for an `HTML` content-type, so that users of your webservice can access an HTML representation of the data they want, ready to be included in any HTML document, e.g. using [jQuery's `load` function](https://api.jquery.com/load/), simply by adding `Accept: text/html` to their request headers. -### Case-studies: *servant-blaze* and *servant-lucid* +### Case-studies: **servant-blaze** and **servant-lucid** These days, most of the haskellers who write their HTML UIs directly from -Haskell use either [blaze-html](http://hackage.haskell.org/package/blaze-html) -or [lucid](http://hackage.haskell.org/package/lucid). The best option for -*servant* is obviously to support both (and hopefully other templating -solutions!). +Haskell use either [**blaze-html**](http://hackage.haskell.org/package/blaze-html) +or [**lucid**](http://hackage.haskell.org/package/lucid). The best option for +**servant** is obviously to support both (and hopefully other templating +solutions!). We're first going to look at **lucid**: ``` haskell data HTMLLucid @@ -483,24 +478,20 @@ data HTMLLucid Once again, the data type is just there as a symbol for the encoding/decoding functions, except that this time we will only worry about encoding since -*blaze-html* and *lucid* don't provide a way to extract data from HTML. - -Both packages also have the same `Accept` instance for their `HTMLLucid` type. +**lucid** doesn't provide a way to extract data from HTML. ``` haskell instance Accept HTMLLucid where contentType _ = "text" // "html" /: ("charset", "utf-8") ``` -Note that this instance uses the `(/:)` operator from *http-media* which lets +Note that this instance uses the `(/:)` operator from **http-media** which lets us specify additional information about a content-type, like the charset here. -The rendering instances for both packages both call similar functions that take +The rendering instances call similar functions that take types with an appropriate instance to an "abstract" HTML representation and then write that to a `ByteString`. -For *lucid*: - ``` haskell instance ToHtml a => MimeRender HTMLLucid a where mimeRender _ = renderBS . toHtml @@ -511,7 +502,7 @@ instance MimeRender HTMLLucid (Html a) where mimeRender _ = renderBS ``` -For *blaze-html*: +For **blaze-html** everything works very similarly: ``` haskell -- For this tutorial to compile 'HTMLLucid' and 'HTMLBlaze' have to be @@ -531,15 +522,13 @@ instance MimeRender HTMLBlaze Text.Blaze.Html.Html where mimeRender _ = renderHtml ``` -Both [servant-blaze](http://hackage.haskell.org/package/servant-blaze) and -[servant-lucid](http://hackage.haskell.org/package/servant-lucid) let you use -`HTMLLucid` in any content type list as long as you provide an instance of the -appropriate class (`ToMarkup` for *blaze-html*, `ToHtml` for *lucid*). +Both [**servant-blaze**](http://hackage.haskell.org/package/servant-blaze) and +[**servant-lucid**](http://hackage.haskell.org/package/servant-lucid) let you use +`HTMLLucid` and `HTMLBlaze` in any content-type list as long as you provide an instance of the +appropriate class (`ToMarkup` for **blaze-html**, `ToHtml` for **lucid**). -We can now write webservice that uses *servant-lucid* to show the `HTMLLucid` -content type in action. First off, imports and pragmas as usual. - -We will be serving the following API: +We can now write a webservice that uses **servant-lucid** to show the `HTMLLucid` +content-type in action. We will be serving the following API: ``` haskell type PersonAPI = "persons" :> Get '[JSON, HTMLLucid] [Person] @@ -556,7 +545,7 @@ data Person = Person instance ToJSON Person ``` -Now, let's teach *lucid* how to render a `Person` as a row in a table, and then +Now, let's teach **lucid** how to render a `Person` as a row in a table, and then a list of `Person`s as a table with a row per person. ``` haskell @@ -600,10 +589,10 @@ server4 :: Server PersonAPI server4 = return people app2 :: Application -app2 = serve personAPI EmptyConfig server4 +app2 = serve personAPI server4 ``` -And we're good to go. You can run this example with `dist/build/tutorial/tutorial 4`. +And we're good to go: ``` bash $ curl http://localhost:8081/persons @@ -616,23 +605,21 @@ And we're good to go. You can run this example with `dist/build/tutorial/tutoria ## The `ExceptT ServantErr IO` monad At the heart of the handlers is the monad they run in, namely `ExceptT -ServantErr IO`. One might wonder: why this monad? The answer is that it is the +ServantErr IO` +([haddock documentation for `ExceptT`](http://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Except.html#t:ExceptT)). +One might wonder: why this monad? The answer is that it is the simplest monad with the following properties: -- it lets us both return a successful result (with the `Right` branch of -`Either`) or "fail" with a descriptive error (with the `Left` branch of -`Either`); +- it lets us both return a successful result (using `return`) +or "fail" with a descriptive error (using `throwError`); - it lets us perform IO, which is absolutely vital since most webservices exist -as interfaces to databases that we interact with in `IO`; +as interfaces to databases that we interact with in `IO`. Let's recall some definitions. ``` haskell ignore --- from the Prelude -data Either e a = Left e | Right a - -- from the 'mtl' package at -newtype ExceptT e m a = ExceptT ( m (Either e a) ) +newtype ExceptT e m a = ExceptT (m (Either e a)) ``` In short, this means that a handler of type `ExceptT ServantErr IO a` is simply @@ -654,14 +641,14 @@ kind and abort early. The next two sections cover how to do just that. Another important instance from the list above is `MonadIO m => MonadIO (ExceptT e m)`. [`MonadIO`](http://hackage.haskell.org/package/transformers-0.4.3.0/docs/Control-Monad-IO-Class.html) -is a class from the *transformers* package defined as: +is a class from the **transformers** package defined as: ``` haskell ignore class Monad m => MonadIO m where liftIO :: IO a -> m a ``` -Obviously, the `IO` monad provides a `MonadIO` instance. Hence for any type +The `IO` monad provides a `MonadIO` instance. Hence for any type `e`, `ExceptT e IO` has a `MonadIO` instance. So if you want to run any kind of IO computation in your handlers, just use `liftIO`: @@ -684,7 +671,7 @@ server5 = do If you want to explicitly fail at providing the result promised by an endpoint using the appropriate HTTP status code (not found, unauthorized, etc) and some -error message, all you have to do is use the `left` function mentioned above +error message, all you have to do is use the `throwError` function mentioned above and provide it with the appropriate value of type `ServantErr`, which is defined as: @@ -703,7 +690,7 @@ use record update syntax: ``` haskell failingHandler :: ExceptT ServantErr IO () -failingHandler = throwE myerr +failingHandler = throwError myerr where myerr :: ServantErr myerr = err503 { errBody = "Sorry dear user." } @@ -718,13 +705,12 @@ server6 = do exists <- liftIO (doesFileExist "myfile.txt") if exists then liftIO (readFile "myfile.txt") >>= return . FileContent - else throwE custom404Err + else throwError custom404Err where custom404Err = err404 { errBody = "myfile.txt just isn't there, please leave this server alone." } ``` -Let's run this server (`dist/build/tutorial/tutorial 5`) and -query it, first without the file and then with the file. +Here's how that server looks in action: ``` bash $ curl --verbose http://localhost:8081/myfile.txt @@ -773,10 +759,10 @@ Note that the type of `addHeader x` is different than the type of `x`! ## Serving static files -*servant-server* also provides a way to just serve the content of a directory +**servant-server** also provides a way to just serve the content of a directory under some path in your web API. As mentioned earlier in this document, the `Raw` combinator can be used in your APIs to mean "plug here any WAI -application". Well, servant-server provides a function to get a file and +application". Well, **servant-server** provides a function to get a file and directory serving WAI application, namely: ``` haskell ignore @@ -784,136 +770,36 @@ directory serving WAI application, namely: serveDirectory :: FilePath -> Server Raw ``` -`serveDirectory`'s argument must be a path to a valid directory. You can see an -example below, runnable with `dist/build/tutorial/tutorial 6` -(you **must** run it from within the *servant-examples/* directory!), which is -a webserver that serves the various bits of code covered in this -getting-started. +`serveDirectory`'s argument must be a path to a valid directory. -The API type will be the following. +Here's an example API that will serve some static files: ``` haskell -type CodeAPI = "code" :> Raw +type StaticAPI = "static" :> Raw ``` And the server: ``` haskell -codeAPI :: Proxy CodeAPI -codeAPI = Proxy +staticAPI :: Proxy StaticAPI +staticAPI = Proxy ``` ``` haskell -server7 :: Server CodeAPI -server7 = serveDirectory "tutorial" +server7 :: Server StaticAPI +server7 = serveDirectory "static-files" app3 :: Application -app3 = serve codeAPI EmptyConfig server7 +app3 = serve staticAPI server7 ``` -This server will match any request whose path starts with `/code` and will look +This server will match any request whose path starts with `/static` and will look for a file at the path described by the rest of the request path, inside the - *tutorial/* directory of the path you run the program from. + *static-files/* directory of the path you run the program from. -In other words: - -- If a client requests `/code/foo.txt`, the server will look for a file at - `./tutorial/foo.txt` (and fail) -- If a client requests `/code/T1.hs`, the server will look for a file at - `./tutorial/T1.hs` (and succeed) -- If a client requests `/code/foo/bar/baz/movie.mp4`, the server will look for - a file at `./tutorial/foo/bar/baz/movie.mp4` (and fail) - -Here is our little server in action. - -``` haskell ignore -$ curl http://localhost:8081/code/T1.hs -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TypeOperators #-} -module T1 where - -import Data.Aeson -import Data.Time.Calendar -import GHC.Generics -import Network.Wai -import Servant - -data User = User - { name :: String - , age :: Int - , email :: String - , registration_date :: Day - } deriving (Eq, Show, Generic) - --- orphan ToJSON instance for Day. necessary to derive one for User -instance ToJSON Day where - -- display a day in YYYY-mm-dd format - toJSON d = toJSON (showGregorian d) - -instance ToJSON User - -type UserAPI = "users" :> Get '[JSON] [User] - -users :: [User] -users = - [ User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1) - , User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1) - ] - -userAPI :: Proxy UserAPI -userAPI = Proxy - -server :: Server UserAPI -server = return users - -app :: Application -app = serve userAPI server -$ curl http://localhost:8081/code/tutorial.hs -import Network.Wai -import Network.Wai.Handler.Warp -import System.Environment - -import qualified T1 -import qualified T2 -import qualified T3 -import qualified T4 -import qualified T5 -import qualified T6 -import qualified T7 -import qualified T9 -import qualified T10 - -app :: String -> (Application -> IO ()) -> IO () -app n f = case n of - "1" -> f T1.app - "2" -> f T2.app - "3" -> f T3.app - "4" -> f T4.app - "5" -> f T5.app - "6" -> f T6.app - "7" -> f T7.app - "8" -> f T3.app - "9" -> T9.writeJSFiles >> f T9.app - "10" -> f T10.app - _ -> usage - -main :: IO () -main = do - args <- getArgs - case args of - [n] -> app n (run 8081) - _ -> usage - -usage :: IO () -usage = do - putStrLn "Usage:\t tutorial N" - putStrLn "\t\twhere N is the number of the example you want to run." - -$ curl http://localhost:8081/foo -not found -``` +In other words: If a client requests `/static/foo.txt`, the server will look for a file at +`./static-files/foo.txt`. If that file exists it'll succeed and serve the file. +If it doesn't exist, the handler will fail with a `404` status code. ## Nested APIs @@ -1123,7 +1009,7 @@ type Server api = ServerT api (ExceptT ServantErr IO) `ServerT` is the actual type family that computes the required types for the handlers that's part of the `HasServer` class. It's like `Server` except that -it takes a third parameter which is the monad you want your handlers to run in, +it takes another parameter which is the monad you want your handlers to run in, or more generally the return types of your handlers. This third parameter is used for specifying the return type of the handler for an endpoint, e.g when computing `ServerT (Get '[JSON] Person) SomeMonad`. The result would be @@ -1131,7 +1017,7 @@ computing `ServerT (Get '[JSON] Person) SomeMonad`. The result would be The first and main question one might have then is: how do we write handlers that run in another monad? How can we "bring back" the value from a given monad -into something *servant* can understand? +into something **servant** can understand? ### Natural transformations @@ -1140,11 +1026,15 @@ do we have? ``` haskell ignore newtype m :~> n = Nat { unNat :: forall a. m a -> n a} - --- For example --- listToMaybeNat ::`[] :~> Maybe` --- listToMaybeNat = Nat listToMaybe -- from Data.Maybe ``` + +For example: + +``` haskell +listToMaybeNat :: [] :~> Maybe +listToMaybeNat = Nat listToMaybe -- from Data.Maybe +``` + (`Nat` comes from "natural transformation", in case you're wondering.) So if you want to write handlers using another monad/type than `ExceptT @@ -1152,20 +1042,20 @@ ServantErr IO`, say the `Reader String` monad, the first thing you have to prepare is a function: ``` haskell ignore -readerToEither :: Reader String :~> ExceptT ServantErr IO +readerToHandler :: Reader String :~> ExceptT ServantErr IO ``` -Let's start with `readerToEither'`. We obviously have to run the `Reader` +Let's start with `readerToHandler'`. We obviously have to run the `Reader` computation by supplying it with a `String`, like `"hi"`. We get an `a` out from that and can then just `return` it into `ExceptT`. We can then just wrap that function with the `Nat` constructor to make it have the fancier type. ``` haskell -readerToEither' :: forall a. Reader String a -> ExceptT ServantErr IO a -readerToEither' r = return (runReader r "hi") +readerToHandler' :: forall a. Reader String a -> ExceptT ServantErr IO a +readerToHandler' r = return (runReader r "hi") -readerToEither :: Reader String :~> ExceptT ServantErr IO -readerToEither = Nat readerToEither' +readerToHandler :: Reader String :~> ExceptT ServantErr IO +readerToHandler = Nat readerToHandler' ``` We can write some simple webservice with the handlers running in `Reader String`. @@ -1193,25 +1083,24 @@ ServantErr IO`. But there's a simple solution to this. ### Enter `enter` -That's right. We have just written `readerToEither`, which is exactly what we -would need to apply to the results of all handlers to make the handlers have the +That's right. We have just written `readerToHandler`, which is exactly what we +would need to apply to all handlers to make the handlers have the right type for `serve`. Being cumbersome to do by hand, we provide a function `enter` which takes a natural transformation between two parametrized types `m` and `n` and a `ServerT someapi m`, and returns a `ServerT someapi n`. In our case, we can wrap up our little webservice by using `enter -readerToEither` on our handlers. +readerToHandler` on our handlers. ``` haskell readerServer :: Server ReaderAPI -readerServer = enter readerToEither readerServerT +readerServer = enter readerToHandler readerServerT app4 :: Application -app4 = serve readerAPI EmptyConfig readerServer +app4 = serve readerAPI readerServer ``` -And we can indeed see this webservice in action by running -`dist/build/tutorial/tutorial 7`. +This is the webservice in action: ``` bash $ curl http://localhost:8081/a @@ -1222,7 +1111,6 @@ $ curl http://localhost:8081/b ## Conclusion -You're now equipped to write any kind of webservice/web-application using -*servant*. One thing not covered here is how to incorporate your own -combinators and will be the topic of a page on the website. The rest of this -document focuses on *servant-client*, *servant-jquery* and *servant-docs*. +You're now equipped to write webservices/web-applications using +**servant**. The rest of this document focuses on **servant-client**, +**servant-js** and **servant-docs**. From 2716d508e83a135336a6825c93fd4c4bb7dc0f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Sun, 28 Feb 2016 22:16:43 +0100 Subject: [PATCH 40/50] tutorial: read through Client.lhs --- doc/tutorial/Client.lhs | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/doc/tutorial/Client.lhs b/doc/tutorial/Client.lhs index 201f187d..9cb38a0e 100644 --- a/doc/tutorial/Client.lhs +++ b/doc/tutorial/Client.lhs @@ -2,7 +2,7 @@ While defining handlers that serve an API has a lot to it, querying an API is simpler: we do not care about what happens inside the webserver, we just need to know how to talk to it and get a response back. Except that we usually have to write the querying functions by hand because the structure of the API isn't a first class citizen and can't be inspected to generate a bunch of client-side functions. -*servant* however has a way to inspect API, because APIs are just Haskell types and (GHC) Haskell lets us do quite a few things with types. In the same way that we look at an API type to deduce the types the handlers should have, we can inspect the structure of the API to *derive* Haskell functions that take one argument for each occurence of `Capture`, `ReqBody`, `QueryParam` +**servant** however has a way to inspect APIs, because APIs are just Haskell types and (GHC) Haskell lets us do quite a few things with types. In the same way that we look at an API type to deduce the types the handlers should have, we can inspect the structure of the API to *derive* Haskell functions that take one argument for each occurence of `Capture`, `ReqBody`, `QueryParam` and friends. By *derive*, we mean that there's no code generation involved, the functions are defined just by the structure of the API type. The source for this tutorial section is a literate haskell file, so first we @@ -67,7 +67,7 @@ type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Posit :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email ``` -What we are going to get with *servant-client* here is 3 functions, one to query each endpoint: +What we are going to get with **servant-client** here is 3 functions, one to query each endpoint: ``` haskell position :: Int -- ^ value for "x" @@ -81,7 +81,15 @@ marketing :: ClientInfo -- ^ value for the request body -> ExceptT ServantError IO Email ``` -Each function makes available as an argument any value that the response may depend on, as evidenced in the API type. How do we get these functions? Just give a `Proxy` to your API and a host to make the requests to: +Each function makes available as an argument any value that the response may +depend on, as evidenced in the API type. How do we get these functions? By calling +the function `client`. It takes three arguments: + +- a `Proxy` to your API, +- a `BaseUrl`, consisting of the protocol, the host, the port and an optional subpath -- + this basically tells `client` where the service that you want to query is hosted, +- a `Manager`, (from [http-client](http://hackage.haskell.org/package/http-client)) +which manages http connections. ``` haskell api :: Proxy API @@ -95,6 +103,9 @@ position :<|> hello :<|> marketing = client api (BaseUrl Http "localhost" 8081 "") __manager ``` +(Yes, the usage of `unsafePerformIO` is very ugly, we know. Hopefully soon it'll +be possible to do without.) + As you can see in the code above, we just "pattern match our way" to these functions. If we try to derive less or more functions than there are endpoints in the API, we obviously get an error. The `BaseUrl` value there is just: ``` haskell ignore @@ -134,16 +145,12 @@ run = do print em ``` -You can now run `dist/build/tutorial/tutorial 8` (the server) and -`dist/build/t8-main/t8-main` (the client) to see them both in action. +Here's the output of the above code running against the appropriate server: ``` bash - $ dist/build/tutorial/tutorial 8 - # and in another terminal: - $ dist/build/t8-main/t8-main - Position {x = 10, y = 10} - HelloMessage {msg = "Hello, servant"} - Email {from = "great@company.com", to = "alp@foo.com", subject = "Hey Alp, we miss you!", body = "Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!"} +Position {x = 10, y = 10} +HelloMessage {msg = "Hello, servant"} +Email {from = "great@company.com", to = "alp@foo.com", subject = "Hey Alp, we miss you!", body = "Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!"} ``` -The types of the arguments for the functions are the same as for (server-side) request handlers. You now know how to use *servant-client*! +The types of the arguments for the functions are the same as for (server-side) request handlers. You now know how to use **servant-client**! From ac02a2852740cb53fc801520144ca8c0b709da45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Sun, 28 Feb 2016 22:21:04 +0100 Subject: [PATCH 41/50] tutorial: formatting for bash sections --- doc/tutorial/Server.lhs | 60 ++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/doc/tutorial/Server.lhs b/doc/tutorial/Server.lhs index ab29b59e..3d1267dc 100644 --- a/doc/tutorial/Server.lhs +++ b/doc/tutorial/Server.lhs @@ -595,11 +595,11 @@ app2 = serve personAPI server4 And we're good to go: ``` bash - $ curl http://localhost:8081/persons - [{"lastName":"Newton","firstName":"Isaac"},{"lastName":"Einstein","firstName":"Albert"}] - $ curl -H 'Accept: text/html' http://localhost:8081/persons -
      first namelast name
      IsaacNewton
      AlbertEinstein
      - # or just point your browser to http://localhost:8081/persons +$ curl http://localhost:8081/persons +[{"lastName":"Newton","firstName":"Isaac"},{"lastName":"Einstein","firstName":"Albert"}] +$ curl -H 'Accept: text/html' http://localhost:8081/persons +
      first namelast name
      IsaacNewton
      AlbertEinstein
      +# or just point your browser to http://localhost:8081/persons ``` ## The `ExceptT ServantErr IO` monad @@ -713,33 +713,33 @@ server6 = do Here's how that server looks in action: ``` bash - $ curl --verbose http://localhost:8081/myfile.txt - [snip] - * Connected to localhost (127.0.0.1) port 8081 (#0) - > GET /myfile.txt HTTP/1.1 - > User-Agent: curl/7.30.0 - > Host: localhost:8081 - > Accept: */* - > - < HTTP/1.1 404 Not Found - [snip] - myfile.txt just isnt there, please leave this server alone. +$ curl --verbose http://localhost:8081/myfile.txt +[snip] +* Connected to localhost (127.0.0.1) port 8081 (#0) +> GET /myfile.txt HTTP/1.1 +> User-Agent: curl/7.30.0 +> Host: localhost:8081 +> Accept: */* +> +< HTTP/1.1 404 Not Found +[snip] +myfile.txt just isnt there, please leave this server alone. - $ echo Hello > myfile.txt +$ echo Hello > myfile.txt - $ curl --verbose http://localhost:8081/myfile.txt - [snip] - * Connected to localhost (127.0.0.1) port 8081 (#0) - > GET /myfile.txt HTTP/1.1 - > User-Agent: curl/7.30.0 - > Host: localhost:8081 - > Accept: */* - > - < HTTP/1.1 200 OK - [snip] - < Content-Type: application/json - [snip] - {"content":"Hello\n"} +$ curl --verbose http://localhost:8081/myfile.txt +[snip] +* Connected to localhost (127.0.0.1) port 8081 (#0) +> GET /myfile.txt HTTP/1.1 +> User-Agent: curl/7.30.0 +> Host: localhost:8081 +> Accept: */* +> +< HTTP/1.1 200 OK +[snip] +< Content-Type: application/json +[snip] +{"content":"Hello\n"} ``` ## Response headers From 8e4ab060308ae698844e9aeab36f680103e9ed29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Sun, 28 Feb 2016 22:32:53 +0100 Subject: [PATCH 42/50] tutorial: read through Docs.lhs --- doc/tutorial/Docs.lhs | 120 +++++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/doc/tutorial/Docs.lhs b/doc/tutorial/Docs.lhs index dbd3233d..fa7b0c43 100644 --- a/doc/tutorial/Docs.lhs +++ b/doc/tutorial/Docs.lhs @@ -26,7 +26,7 @@ import Servant.Server ``` And we'll import some things from one of our earlier modules -([Serving an API](/tutorial/server.html)): +([Serving an API](Server.html)): ``` haskell import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..), @@ -35,7 +35,7 @@ import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..), Like client function generation, documentation generation amounts to inspecting the API type and extracting all the data we need to then present it in some format to users of your API. -This time however, we have to assist *servant*. While it is able to deduce a lot of things about our API, it can't magically come up with descriptions of the various pieces of our APIs that are human-friendly and explain what's going on "at the business-logic level". A good example to study for documentation generation is our webservice with the `/position`, `/hello` and `/marketing` endpoints from earlier: +This time however, we have to assist **servant**. While it is able to deduce a lot of things about our API, it can't magically come up with descriptions of the various pieces of our APIs that are human-friendly and explain what's going on "at the business-logic level". A good example to study for documentation generation is our webservice with the `/position`, `/hello` and `/marketing` endpoints from earlier: ``` haskell type ExampleAPI = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position @@ -46,7 +46,7 @@ exampleAPI :: Proxy ExampleAPI exampleAPI = Proxy ``` -While *servant* can see e.g. that there are 3 endpoints and that the response bodies will be in JSON, it doesn't know what influence the captures, parameters, request bodies and other combinators have on the webservice. This is where some manual work is required. +While **servant** can see e.g. that there are 3 endpoints and that the response bodies will be in JSON, it doesn't know what influence the captures, parameters, request bodies and other combinators have on the webservice. This is where some manual work is required. For every capture, request body, response body, query param, we have to give some explanations about how it influences the response, what values are possible and the likes. Here's how it looks like for the parameters we have above. @@ -97,9 +97,9 @@ apiDocs :: API apiDocs = docs exampleAPI ``` -`API` is a type provided by *servant-docs* that stores all the information one needs about a web API in order to generate documentation in some format. Out of the box, *servant-docs* only provides a pretty documentation printer that outputs [Markdown](http://en.wikipedia.org/wiki/Markdown), but the [servant-pandoc](http://hackage.haskell.org/package/servant-pandoc) package can be used to target many useful formats. +`API` is a type provided by **servant-docs** that stores all the information one needs about a web API in order to generate documentation in some format. Out of the box, **servant-docs** only provides a pretty documentation printer that outputs [Markdown](http://en.wikipedia.org/wiki/Markdown), but the [**servant-pandoc**](http://hackage.haskell.org/package/servant-pandoc) package can be used to target many useful formats. -*servant*'s markdown pretty printer is a function named `markdown`. +**servant**'s markdown pretty printer is a function named `markdown`. ``` haskell ignore markdown :: API -> String @@ -107,97 +107,97 @@ markdown :: API -> String That lets us see what our API docs look down in markdown, by looking at `markdown apiDocs`. -``` text - ## Welcome +````````` text +## Welcome - This is our super webservice's API. +This is our super webservice's API. - Enjoy! +Enjoy! - ## GET /hello +## GET /hello - #### GET Parameters: +#### GET Parameters: - - name - - **Values**: *Alp, John Doe, ...* - - **Description**: Name of the person to say hello to. +- name + - **Values**: *Alp, John Doe, ...* + - **Description**: Name of the person to say hello to. - #### Response: +#### Response: - - Status code 200 - - Headers: [] +- Status code 200 +- Headers: [] - - Supported content types are: +- Supported content types are: - - `application/json` + - `application/json` - - When a value is provided for 'name' +- When a value is provided for 'name' - ```javascript - {"msg":"Hello, Alp"} - ``` + ```javascript + {"msg":"Hello, Alp"} + ``` - - When 'name' is not specified +- When 'name' is not specified - ```javascript - {"msg":"Hello, anonymous coward"} - ``` + ```javascript + {"msg":"Hello, anonymous coward"} + ``` - ## POST /marketing +## POST /marketing - #### Request: +#### Request: - - Supported content types are: +- Supported content types are: - - `application/json` + - `application/json` - - Example: `application/json` +- Example: `application/json` - ```javascript - {"email":"alp@foo.com","interested_in":["haskell","mathematics"],"age":26,"name":"Alp"} - ``` + ```javascript + {"email":"alp@foo.com","interested_in":["haskell","mathematics"],"age":26,"name":"Alp"} + ``` - #### Response: +#### Response: - - Status code 201 - - Headers: [] +- Status code 201 +- Headers: [] - - Supported content types are: +- Supported content types are: - - `application/json` + - `application/json` - - Response body as below. +- Response body as below. - ```javascript - {"subject":"Hey Alp, we miss you!","body":"Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"} - ``` + ```javascript + {"subject":"Hey Alp, we miss you!","body":"Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"} + ``` - ## GET /position/:x/:y +## GET /position/:x/:y - #### Captures: +#### Captures: - - *x*: (integer) position on the x axis - - *y*: (integer) position on the y axis +- *x*: (integer) position on the x axis +- *y*: (integer) position on the y axis - #### Response: +#### Response: - - Status code 200 - - Headers: [] +- Status code 200 +- Headers: [] - - Supported content types are: +- Supported content types are: - - `application/json` + - `application/json` - - Response body as below. +- Response body as below. - ```javascript - {"x":3,"y":14} - ``` + ```javascript + {"x":3,"y":14} + ``` -``` +````````` -However, we can also add one or more introduction sections to the document. We just need to tweak the way we generate `apiDocs`. We will also convert the content to a lazy `ByteString` since this is what *wai* expects for `Raw` endpoints. +However, we can also add one or more introduction sections to the document. We just need to tweak the way we generate `apiDocs`. We will also convert the content to a lazy `ByteString` since this is what **wai** expects for `Raw` endpoints. ``` haskell docsBS :: ByteString @@ -231,4 +231,4 @@ app :: Application app = serve api server ``` -And if you spin up this server with `dist/build/tutorial/tutorial 10` and go to anywhere else than `/position`, `/hello` and `/marketing`, you will see the API docs in markdown. This is because `serveDocs` is attempted if the 3 other endpoints don't match and systematically succeeds since its definition is to just return some fixed bytestring with the `text/plain` content type. +And if you spin up this server and request anything else than `/position`, `/hello` and `/marketing`, you will see the API docs in markdown. This is because `serveDocs` is attempted if the 3 other endpoints don't match and systematically succeeds since its definition is to just return some fixed bytestring with the `text/plain` content type. From 1b928878dd6248091196925b82e2f55a79f6cd03 Mon Sep 17 00:00:00 2001 From: rwobben Date: Tue, 1 Mar 2016 07:16:26 +0000 Subject: [PATCH 43/50] added aeson-compat to the cabal file --- doc/tutorial/tutorial.cabal | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index a39602c1..189140de 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -24,7 +24,8 @@ library build-depends: base == 4.* , base-compat , text - , aeson >= 0.11 + , aeson + , aeson-compat , blaze-html , directory , blaze-markup From 027cd827252061a39d40dd443c28bdbe28709b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Tue, 1 Mar 2016 19:47:14 +0800 Subject: [PATCH 44/50] tutorial: corrected curl examples --- doc/tutorial/Server.lhs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/tutorial/Server.lhs b/doc/tutorial/Server.lhs index 3d1267dc..1a3db276 100644 --- a/doc/tutorial/Server.lhs +++ b/doc/tutorial/Server.lhs @@ -297,12 +297,12 @@ And that's it. Here's the example in action: ``` bash $ curl http://localhost:8081/position/1/2 -{"x":1,"y":2} +{"xCoord":1,"yCoord":2} $ curl http://localhost:8081/hello {"msg":"Hello, anonymous coward"} $ curl http://localhost:8081/hello?name=Alp {"msg":"Hello, Alp"} -$ curl -X POST -d '{"name":"Alp Mestanogullari", "email" : "alp@foo.com", "age": 25, "interested_in": ["haskell", "mathematics"]}' -H 'Accept: application/json' -H 'Content-type: application/json' http://localhost:8081/marketing +$ curl -X POST -d '{"clientName":"Alp Mestanogullari", "clientEmail" : "alp@foo.com", "clientAge": 25, "clientInterestedIn": ["haskell", "mathematics"]}' -H 'Accept: application/json' -H 'Content-type: application/json' http://localhost:8081/marketing {"subject":"Hey Alp Mestanogullari, we miss you!","body":"Hi Alp Mestanogullari,\n\nSince you've recently turned 25, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"} ``` From 0985e510225b3697bf54c004a7e5333251217abe Mon Sep 17 00:00:00 2001 From: rwobben Date: Wed, 2 Mar 2016 13:03:18 +0000 Subject: [PATCH 45/50] deleted the Trans.Monad.Except --- doc/tutorial/Server.lhs | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/tutorial/Server.lhs b/doc/tutorial/Server.lhs index 1a3db276..bd84b8a0 100644 --- a/doc/tutorial/Server.lhs +++ b/doc/tutorial/Server.lhs @@ -28,7 +28,6 @@ import Prelude.Compat import Control.Monad.Except import Control.Monad.Reader -import Control.Monad.Trans.Except import Data.Aeson.Compat import Data.Aeson.Types import Data.Attoparsec.ByteString From b97a352773d7be6ff14dc272511451ed4a408d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Sat, 12 Mar 2016 16:03:57 +0800 Subject: [PATCH 46/50] tutorial: updated Javascript.lhs (and wrote some tests for it) --- doc/tutorial/.ghci | 2 +- doc/tutorial/Javascript.lhs | 106 ++++++++++++++++++++-------- doc/tutorial/static/index.html | 26 +++++++ doc/tutorial/static/ui.js | 60 ++++++++++++++++ doc/tutorial/test/JavascriptSpec.hs | 32 +++++++++ doc/tutorial/test/Spec.hs | 1 + doc/tutorial/tutorial.cabal | 19 ++--- 7 files changed, 206 insertions(+), 40 deletions(-) create mode 100644 doc/tutorial/static/index.html create mode 100644 doc/tutorial/static/ui.js create mode 100644 doc/tutorial/test/JavascriptSpec.hs create mode 100644 doc/tutorial/test/Spec.hs diff --git a/doc/tutorial/.ghci b/doc/tutorial/.ghci index 7d8e760c..d8e88521 100644 --- a/doc/tutorial/.ghci +++ b/doc/tutorial/.ghci @@ -1 +1 @@ -:set -pgmL markdown-unlit -Wall -Werror -fno-warn-missing-methods -fno-warn-name-shadowing +:set -pgmL markdown-unlit -Wall -Werror -fno-warn-missing-methods -fno-warn-name-shadowing -itest diff --git a/doc/tutorial/Javascript.lhs b/doc/tutorial/Javascript.lhs index 600c8327..4054e4b3 100644 --- a/doc/tutorial/Javascript.lhs +++ b/doc/tutorial/Javascript.lhs @@ -1,9 +1,7 @@ # Generating Javascript functions to query an API -We will now see how *servant* lets you turn an API type into javascript -functions that you can call to query a webservice. The derived code assumes you -use *jQuery* but you could very easily adapt the code to generate ajax requests -based on vanilla javascript or another library than *jQuery*. +We will now see how **servant** lets you turn an API type into javascript +functions that you can call to query a webservice. For this, we will consider a simple page divided in two parts. At the top, we will have a search box that lets us search in a list of Haskell books by @@ -32,10 +30,11 @@ import Data.Aeson import Data.Proxy import Data.Text as T (Text) import Data.Text.IO as T (writeFile, readFile) -import qualified Data.Text as T import GHC.Generics import Language.Javascript.JQuery import Network.Wai +import Network.Wai.Handler.Warp +import qualified Data.Text as T import Servant import Servant.JS import System.Random @@ -78,7 +77,8 @@ book :: Text -> Text -> Int -> Book book = Book ``` -We need a "book database". For the purpose of this guide, let's restrict ourselves to the following books. +We need a "book database". For the purpose of this guide, let's restrict +ourselves to the following books. ``` haskell books :: [Book] @@ -92,7 +92,10 @@ books = ] ``` -Now, given an optional search string `q`, we want to perform a case insensitive search in that list of books. We're obviously not going to try and implement the best possible algorithm, this is out of scope for this tutorial. The following simple linear scan will do, given how small our list is. +Now, given an optional search string `q`, we want to perform a case insensitive +search in that list of books. We're obviously not going to try and implement +the best possible algorithm, this is out of scope for this tutorial. The +following simple linear scan will do, given how small our list is. ``` haskell searchBook :: Monad m => Maybe Text -> m (Search Book) @@ -106,7 +109,9 @@ searchBook (Just q) = return (mkSearch q books') q' = T.toLower q ``` -We also need an endpoint that generates random points `(x, y)` with `-1 <= x,y <= 1`. The code below uses [random](http://hackage.haskell.org/package/random)'s `System.Random`. +We also need an endpoint that generates random points `(x, y)` with `-1 <= x,y +<= 1`. The code below uses +[random](http://hackage.haskell.org/package/random)'s `System.Random`. ``` haskell randomPoint :: MonadIO m => m Point @@ -131,54 +136,93 @@ server = randomPoint server' :: Server API' server' = server - :<|> serveDirectory "tutorial/t9" + :<|> serveDirectory "static" app :: Application app = serve api' server' + +main :: IO () +main = run 8000 app ``` -Why two different API types, proxies and servers though? Simply because we don't want to generate javascript functions for the `Raw` part of our API type, so we need a `Proxy` for our API type `API'` without its `Raw` endpoint. +Why two different API types, proxies and servers though? Simply because we +don't want to generate javascript functions for the `Raw` part of our API type, +so we need a `Proxy` for our API type `API'` without its `Raw` endpoint. -Very similarly to how one can derive haskell functions, we can derive the javascript with just a simple function call to `jsForAPI` from `Servant.JQuery`. +Very similarly to how one can derive haskell functions, we can derive the +javascript with just a simple function call to `jsForAPI` from +`Servant.JQuery`. ``` haskell apiJS :: Text apiJS = jsForAPI api vanillaJS ``` -This `String` contains 2 Javascript functions: +This `Text` contains 2 Javascript functions, 'getPoint' and 'getBooks': ``` javascript - -function getpoint(onSuccess, onError) +var getPoint = function(onSuccess, onError) { - $.ajax( - { url: '/point' - , success: onSuccess - , error: onError - , method: 'GET' - }); + var xhr = new XMLHttpRequest(); + xhr.open('GET', '/point', true); + xhr.setRequestHeader("Accept","application/json"); + xhr.onreadystatechange = function (e) { + if (xhr.readyState == 4) { + if (xhr.status == 204 || xhr.status == 205) { + onSuccess(); + } else if (xhr.status >= 200 && xhr.status < 300) { + var value = JSON.parse(xhr.responseText); + onSuccess(value); + } else { + var value = JSON.parse(xhr.responseText); + onError(value); + } + } + } + xhr.send(null); } -function getbooks(q, onSuccess, onError) +var getBooks = function(q, onSuccess, onError) { - $.ajax( - { url: '/books' + '?q=' + encodeURIComponent(q) - , success: onSuccess - , error: onError - , method: 'GET' - }); + var xhr = new XMLHttpRequest(); + xhr.open('GET', '/books' + '?q=' + encodeURIComponent(q), true); + xhr.setRequestHeader("Accept","application/json"); + xhr.onreadystatechange = function (e) { + if (xhr.readyState == 4) { + if (xhr.status == 204 || xhr.status == 205) { + onSuccess(); + } else if (xhr.status >= 200 && xhr.status < 300) { + var value = JSON.parse(xhr.responseText); + onSuccess(value); + } else { + var value = JSON.parse(xhr.responseText); + onError(value); + } + } + } + xhr.send(null); } ``` -Right before starting up our server, we will need to write this `String` to a file, say `api.js`, along with a copy of the *jQuery* library, as provided by the [js-jquery](http://hackage.haskell.org/package/js-jquery) package. +We created a directory `static` that contains two static files: `index.html`, +which is the entrypoint to our little web application; and `ui.js`, which +contains some hand-written javascript. This javascript code assumes the two +generated functions `getPoint` and `getBooks` in scope. Therefore we need to +write the generated javascript into a file: ``` haskell writeJSFiles :: IO () writeJSFiles = do - T.writeFile "getting-started/gs9/api.js" apiJS + T.writeFile "static/api.js" apiJS jq <- T.readFile =<< Language.Javascript.JQuery.file - T.writeFile "getting-started/gs9/jq.js" jq + T.writeFile "static/jq.js" jq ``` -And we're good to go. Start the server with `dist/build/tutorial/tutorial 9` and go to `http://localhost:8081/`. Start typing in the name of one of the authors in our database or part of a book title, and check out how long it takes to approximate π using the method mentioned above. +(We're also writing the jquery library into a file, as it's also used by +`ui.js`.) `static/api.js` will be included in `index.html` and the two +generated functions will therefore be available in `ui.js`. + +And we're good to go. You can start the `main` function of this file and go to +`http://localhost:8000/`. Start typing in the name of one of the authors in our +database or part of a book title, and check out how long it takes to +approximate pi using the method mentioned above. diff --git a/doc/tutorial/static/index.html b/doc/tutorial/static/index.html new file mode 100644 index 00000000..6a047c1c --- /dev/null +++ b/doc/tutorial/static/index.html @@ -0,0 +1,26 @@ + + + + + + Tutorial - 9 - servant-jquery + + +

      Books

      + +
      +

      Results for ""

      +
        +
      +
      +
      +

      Approximating π

      +

      Count: 0

      +

      Successes: 0

      +

      + + + + + + diff --git a/doc/tutorial/static/ui.js b/doc/tutorial/static/ui.js new file mode 100644 index 00000000..8bcae8d8 --- /dev/null +++ b/doc/tutorial/static/ui.js @@ -0,0 +1,60 @@ +/* book search */ +function updateResults(data) +{ + console.log(data); + $('#results').html(""); + $('#query').text("\"" + data.query + "\""); + for(var i = 0; i < data.results.length; i++) + { + $('#results').append(renderBook(data.results[i])); + } +} + +function renderBook(book) +{ + var li = '
    • ' + book.title + ', ' + + book.author + ' - ' + book.year + '
    • '; + return li; +} + +function searchBooks() +{ + var q = $('#q').val(); + getBooks(q, updateResults, console.log) +} + +searchBooks(); +$('#q').keyup(function() { + searchBooks(); +}); + +/* approximating pi */ +var count = 0; +var successes = 0; + +function f(data) +{ + var x = data.x, y = data.y; + if(x*x + y*y <= 1) + { + successes++; + } + + count++; + + update('#count', count); + update('#successes', successes); + update('#pi', 4*successes/count); +} + +function update(id, val) +{ + $(id).text(val); +} + +function refresh() +{ + getPoint(f, console.log); +} + +window.setInterval(refresh, 200); diff --git a/doc/tutorial/test/JavascriptSpec.hs b/doc/tutorial/test/JavascriptSpec.hs new file mode 100644 index 00000000..2d6007a5 --- /dev/null +++ b/doc/tutorial/test/JavascriptSpec.hs @@ -0,0 +1,32 @@ +{-# LANGUAGE OverloadedStrings #-} + +module JavascriptSpec where + +import Data.List +import Data.String +import Data.String.Conversions +import Test.Hspec +import Test.Hspec.Wai + +import Javascript + +spec :: Spec +spec = do + describe "apiJS" $ do + it "is contained verbatim in Javascript.lhs" $ do + code <- readFile "Javascript.lhs" + cs apiJS `shouldSatisfy` (`isInfixOf` code) + + describe "writeJSFiles" $ do + it "[not a test] write apiJS to static/api.js" $ do + writeJSFiles + + describe "app" $ with (return app) $ do + context "/api.js" $ do + it "delivers apiJS" $ do + get "/api.js" `shouldRespondWith` (fromString (cs apiJS)) + + context "/" $ do + it "delivers something" $ do + get "" `shouldRespondWith` 200 + get "/" `shouldRespondWith` 200 diff --git a/doc/tutorial/test/Spec.hs b/doc/tutorial/test/Spec.hs new file mode 100644 index 00000000..a824f8c3 --- /dev/null +++ b/doc/tutorial/test/Spec.hs @@ -0,0 +1 @@ +{-# OPTIONS_GHC -F -pgmF hspec-discover #-} diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index 189140de..9664ce45 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -1,16 +1,12 @@ name: tutorial version: 0.5 synopsis: The servant tutorial --- description: homepage: http://haskell-servant.github.io/ license: BSD3 license-file: LICENSE author: Servant Contributors maintainer: haskell-servant-maintainers@googlegroups.com --- copyright: --- category: build-type: Simple --- extra-source-files: cabal-version: >=1.10 library @@ -19,13 +15,11 @@ library , Docs , Javascript , Server - -- other-modules: - -- other-extensions: build-depends: base == 4.* , base-compat , text , aeson - , aeson-compat + , aeson-compat , blaze-html , directory , blaze-markup @@ -49,9 +43,18 @@ library , transformers , markdown-unlit >= 0.4 , http-client - -- hs-source-dirs: default-language: Haskell2010 ghc-options: -Wall -Werror -pgmL markdown-unlit -- to silence aeson-0.10 warnings: ghc-options: -fno-warn-missing-methods ghc-options: -fno-warn-name-shadowing + +test-suite spec + type: exitcode-stdio-1.0 + ghc-options: + -Wall -fno-warn-name-shadowing -fno-warn-missing-signatures + default-language: Haskell2010 + hs-source-dirs: test + main-is: Spec.hs + build-depends: + base == 4.* From 4e90308b854fb66df1f199ad4357b30f783ccee2 Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Fri, 18 Mar 2016 15:09:38 +0100 Subject: [PATCH 47/50] Fix compilation --- doc/tutorial/tutorial.cabal | 7 +++++-- stack.yaml | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/tutorial/tutorial.cabal b/doc/tutorial/tutorial.cabal index 9664ce45..7608a60c 100644 --- a/doc/tutorial/tutorial.cabal +++ b/doc/tutorial/tutorial.cabal @@ -56,5 +56,8 @@ test-suite spec default-language: Haskell2010 hs-source-dirs: test main-is: Spec.hs - build-depends: - base == 4.* + build-depends: base == 4.* + , tutorial + , hspec + , hspec-wai + , string-conversions diff --git a/stack.yaml b/stack.yaml index 40ddab48..947970a5 100644 --- a/stack.yaml +++ b/stack.yaml @@ -21,4 +21,4 @@ extra-deps: - markdown-unlit-0.4.0 - aeson-0.11.0.0 - fail-4.9.0.0 -resolver: nightly-2015-10-08 +resolver: nightly-2016-03-17 From 79029089db3031b6232f3e349e73045522dfc517 Mon Sep 17 00:00:00 2001 From: "Julian K. Arni" Date: Fri, 18 Mar 2016 15:10:56 +0100 Subject: [PATCH 48/50] Remove servant-examples (again) --- .../auth-combinator/auth-combinator.hs | 124 --------------- servant-examples/basic-auth/basic-auth.hs | 105 ------------- servant-examples/servant-examples.cabal | 148 ------------------ .../socket-io-chat/socket-io-chat.hs | 54 ------- servant-examples/tutorial/T1.hs | 45 ------ servant-examples/tutorial/T10.hs | 71 --------- servant-examples/tutorial/T2.hs | 52 ------ servant-examples/tutorial/T3.hs | 84 ---------- servant-examples/tutorial/T4.hs | 63 -------- servant-examples/tutorial/T5.hs | 37 ----- servant-examples/tutorial/T6.hs | 18 --- servant-examples/tutorial/T7.hs | 33 ---- servant-examples/tutorial/T9.hs | 105 ------------- .../wai-middleware/wai-middleware.hs | 51 ------ 14 files changed, 990 deletions(-) delete mode 100644 servant-examples/auth-combinator/auth-combinator.hs delete mode 100644 servant-examples/basic-auth/basic-auth.hs delete mode 100644 servant-examples/servant-examples.cabal delete mode 100644 servant-examples/socket-io-chat/socket-io-chat.hs delete mode 100644 servant-examples/tutorial/T1.hs delete mode 100644 servant-examples/tutorial/T10.hs delete mode 100644 servant-examples/tutorial/T2.hs delete mode 100644 servant-examples/tutorial/T3.hs delete mode 100644 servant-examples/tutorial/T4.hs delete mode 100644 servant-examples/tutorial/T5.hs delete mode 100644 servant-examples/tutorial/T6.hs delete mode 100644 servant-examples/tutorial/T7.hs delete mode 100644 servant-examples/tutorial/T9.hs delete mode 100644 servant-examples/wai-middleware/wai-middleware.hs diff --git a/servant-examples/auth-combinator/auth-combinator.hs b/servant-examples/auth-combinator/auth-combinator.hs deleted file mode 100644 index 709efa0c..00000000 --- a/servant-examples/auth-combinator/auth-combinator.hs +++ /dev/null @@ -1,124 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} -{-# LANGUAGE UndecidableInstances #-} - -import Control.Monad.Trans.Except (ExceptT, throwE) -import Data.Aeson hiding ((.:)) -import Data.ByteString (ByteString) -import Data.Monoid ((<>)) -import Data.Map (Map, fromList) -import qualified Data.Map as Map -import Data.Text (Text) -import GHC.Generics -import Network.Wai -import Network.Wai.Handler.Warp -import Servant -import Servant.Server.Experimental.Auth - --- | This file contains an authenticated server using servant's generalized --- authentication support. Our basic authentication scheme is trivial: we --- look for a cookie named "servant-auth-cookie" and its value will contain --- a key, which we use to lookup a User. Obviously this is an absurd example, --- but we pick something simple and non-standard to show you how to extend --- servant's support for authentication. - --- | A user type that we "fetch from the database" after --- performing authentication -newtype User = User { unUser :: Text } - --- | A (pure) database mapping keys to users. -database :: Map ByteString User -database = fromList [ ("key1", User "Anne Briggs") - , ("key2", User "Bruce Cockburn") - , ("key3", User "Ghédalia Tazartès") - ] - --- | A method that, when given a password, will return a User. --- This is our bespoke (and bad) authentication logic. -lookupUser :: ByteString -> ExceptT ServantErr IO User -lookupUser key = case Map.lookup key database of - Nothing -> throwE (err403 { errBody = "Invalid Cookie" }) - Just usr -> return usr - --- | The auth handler wraps a function from Request -> ExceptT ServantErr IO User --- we look for a Cookie and pass the value of the cookie to `lookupUser`. -authHandler :: AuthHandler Request User -authHandler = - let handler req = case lookup "servant-auth-cookie" (requestHeaders req) of - Nothing -> throwE (err401 { errBody = "Missing auth header" }) - Just authCookieKey -> lookupUser authCookieKey - in mkAuthHandler handler - --- | Data types that will be returned from various api endpoints -newtype PrivateData = PrivateData { ssshhh :: Text } - deriving (Eq, Show, Generic) - -instance ToJSON PrivateData - -newtype PublicData = PublicData { somedata :: Text } - deriving (Eq, Show, Generic) - -instance ToJSON PublicData - --- | Our private API that we want to be auth-protected. -type PrivateAPI = Get '[JSON] [PrivateData] - --- | Our public API that doesn't have any protection -type PublicAPI = Get '[JSON] [PublicData] - --- | Our API, with auth-protection -type API = "private" :> AuthProtect "cookie-auth" :> PrivateAPI - :<|> "public" :> PublicAPI - --- | A value holding our type-level API -api :: Proxy API -api = Proxy - --- | We need to specify the data returned after authentication -type instance AuthServerData (AuthProtect "cookie-auth") = User - --- | The context that will be made available to request handlers. We supply the --- "cookie-auth"-tagged request handler defined above, so that the 'HasServer' instance --- of 'AuthProtect' can extract the handler and run it on the request. -serverContext :: Context (AuthHandler Request User ': '[]) -serverContext = authHandler :. EmptyContext - --- | Our API, where we provide all the author-supplied handlers for each end --- point. Note that 'privateDataFunc' is a function that takes 'User' as an --- argument. We dont' worry about the authentication instrumentation here, --- that is taken care of by supplying context -server :: Server API -server = privateDataFunc :<|> return publicData - - where privateDataFunc (User name) = - return [PrivateData ("this is a secret: " <> name)] - publicData = [PublicData "this is a public piece of data"] - --- | run our server -main :: IO () -main = run 8080 (serveWithContext api serverContext server) - -{- Sample Session: - -$ curl -XGET localhost:8080/private -Missing auth header ->>>>>>> modify auth-combinator example for gen auth ->>>>>>> 8246c1f... modify auth-combinator example for gen auth - -$ curl -XGET localhost:8080/private -H "servant-auth-cookie: key3" -[{"ssshhh":"this is a secret: Ghédalia Tazartès"}] - -$ curl -XGET localhost:8080/private -H "servant-auth-cookie: bad-key" -Invalid Cookie - -$ curl -XGET localhost:8080/public -[{"somedata":"this is a public piece of data"}] --} - diff --git a/servant-examples/basic-auth/basic-auth.hs b/servant-examples/basic-auth/basic-auth.hs deleted file mode 100644 index cedd4694..00000000 --- a/servant-examples/basic-auth/basic-auth.hs +++ /dev/null @@ -1,105 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeOperators #-} - -module Main where - -import Data.Aeson (ToJSON) -import Data.Proxy (Proxy (Proxy)) -import Data.Text (Text) -import GHC.Generics (Generic) -import Network.Wai.Handler.Warp (run) -import Servant.API ((:<|>) ((:<|>)), (:>), BasicAuth, - Get, JSON) -import Servant.API.BasicAuth (BasicAuthData (BasicAuthData)) -import Servant.Server (BasicAuthCheck (BasicAuthCheck), - BasicAuthResult( Authorized - , Unauthorized - ), - Context ((:.), EmptyContext), Server, - serveWithContext) - --- | let's define some types that our API returns. - --- | private data that needs protection -newtype PrivateData = PrivateData { ssshhh :: Text } - deriving (Eq, Show, Generic) - -instance ToJSON PrivateData - --- | public data that anyone can use. -newtype PublicData = PublicData { somedata :: Text } - deriving (Eq, Show, Generic) - -instance ToJSON PublicData - --- | A user we'll grab from the database when we authenticate someone -newtype User = User { userName :: Text } - deriving (Eq, Show) - --- | a type to wrap our public api -type PublicAPI = Get '[JSON] [PublicData] - --- | a type to wrap our private api -type PrivateAPI = Get '[JSON] PrivateData - --- | our API -type API = "public" :> PublicAPI - :<|> "private" :> BasicAuth "foo-realm" User :> PrivateAPI - --- | a value holding a proxy of our API type -api :: Proxy API -api = Proxy - --- | 'BasicAuthCheck' holds the handler we'll use to verify a username and password. -authCheck :: BasicAuthCheck User -authCheck = - let check (BasicAuthData username password) = - if username == "servant" && password == "server" - then return (Authorized (User "servant")) - else return Unauthorized - in BasicAuthCheck check - --- | We need to supply our handlers with the right Context. In this case, --- Basic Authentication requires a Context Entry with the 'BasicAuthCheck' value --- tagged with "foo-tag" This context is then supplied to 'server' and threaded --- to the BasicAuth HasServer handlers. -serverContext :: Context (BasicAuthCheck User ': '[]) -serverContext = authCheck :. EmptyContext - --- | an implementation of our server. Here is where we pass all the handlers to our endpoints. --- In particular, for the BasicAuth protected handler, we need to supply a function --- that takes 'User' as an argument. -server :: Server API -server = - let publicAPIHandler = return [PublicData "foo", PublicData "bar"] - privateAPIHandler (user :: User) = return (PrivateData (userName user)) - in publicAPIHandler :<|> privateAPIHandler - --- | hello, server! -main :: IO () -main = run 8080 (serveWithContext api serverContext server) - -{- Sample session - -$ curl -XGET localhost:8080/public -[{"somedata":"foo"},{"somedata":"bar"} - -$ curl -iXGET localhost:8080/private -HTTP/1.1 401 Unauthorized -transfer-encoding: chunked -Date: Thu, 07 Jan 2016 22:36:38 GMT -Server: Warp/3.1.8 -WWW-Authenticate: Basic realm="foo-realm" - -$ curl -iXGET localhost:8080/private -H "Authorization: Basic c2VydmFudDpzZXJ2ZXI=" -HTTP/1.1 200 OK -transfer-encoding: chunked -Date: Thu, 07 Jan 2016 22:37:58 GMT -Server: Warp/3.1.8 -Content-Type: application/json - -{"ssshhh":"servant"} --} diff --git a/servant-examples/servant-examples.cabal b/servant-examples/servant-examples.cabal deleted file mode 100644 index 1f00349e..00000000 --- a/servant-examples/servant-examples.cabal +++ /dev/null @@ -1,148 +0,0 @@ -name: servant-examples -version: 0.5 -synopsis: Example programs for servant -description: Example programs for servant, - showcasing solutions to common needs. -homepage: http://haskell-servant.github.io/ -license: BSD3 -license-file: LICENSE -author: Servant Contributors -maintainer: haskell-servant-maintainers@googlegroups.com -copyright: 2015-2016 Servant Contributors -category: Web -build-type: Simple -cabal-version: >=1.10 -bug-reports: http://github.com/haskell-servant/servant/issues -source-repository head - type: git - location: http://github.com/haskell-servant/servant.git - -executable tutorial - main-is: tutorial.hs - other-modules: T1, T2, T3, T4, T5, T6, T7, T8, T9, T10 - ghc-options: -Wall -fno-warn-unused-binds -fno-warn-name-shadowing -fno-warn-orphans -fno-warn-unused-imports - build-depends: - aeson >= 0.8 - , base >= 4.7 && < 5 - , bytestring - , directory - , http-types - , js-jquery - , lucid - , random - , servant == 0.5.* - , servant-docs == 0.5.* - , servant-js == 0.5.* - , servant-lucid == 0.5.* - , servant-server == 0.5.* - , text - , time - , transformers - , transformers-compat - , wai - , warp - hs-source-dirs: tutorial - default-language: Haskell2010 - -executable t8-main - main-is: t8-main.hs - other-modules: T3, T8 - hs-source-dirs: tutorial - default-language: Haskell2010 - ghc-options: -Wall -fno-warn-unused-binds -fno-warn-name-shadowing - build-depends: - aeson - , base >= 4.7 && < 5 - , http-client > 0.4 && < 0.5 - , servant == 0.5.* - , servant-client == 0.5.* - , servant-server == 0.5.* - , transformers - , transformers-compat - , wai - -executable hackage - main-is: hackage.hs - build-depends: - aeson >= 0.8 - , base >=4.7 && < 5 - , http-client > 0.4 && < 0.5 - , servant == 0.5.* - , servant-client == 0.5.* - , text - , transformers - , transformers-compat - hs-source-dirs: hackage - default-language: Haskell2010 - -executable wai-middleware - main-is: wai-middleware.hs - build-depends: - aeson >= 0.8 - , base >= 4.7 && < 5 - , servant == 0.5.* - , servant-server == 0.5.* - , text - , wai - , wai-extra - , warp - hs-source-dirs: wai-middleware - default-language: Haskell2010 - -executable basic-auth - main-is: basic-auth.hs - ghc-options: -Wall -fno-warn-unused-binds -fno-warn-name-shadowing - build-depends: - aeson >= 0.8 - , base >= 4.7 && < 5 - , bytestring - , http-types - , servant == 0.5.* - , servant-server == 0.5.* - , text - , wai - , warp - hs-source-dirs: basic-auth - default-language: Haskell2010 - -executable auth-combinator - main-is: auth-combinator.hs - ghc-options: -Wall -fno-warn-unused-binds -fno-warn-name-shadowing - build-depends: - aeson >= 0.8 - , base >= 4.7 && < 5 - , bytestring - , containers - , http-types - , servant == 0.5.* - , servant-server == 0.5.* - , text - , transformers - , wai - , warp - hs-source-dirs: auth-combinator - default-language: Haskell2010 - -executable socket-io-chat - main-is: socket-io-chat.hs - ghc-options: -Wall -fno-warn-unused-binds -fno-warn-name-shadowing - other-modules: Chat - build-depends: - aeson >= 0.8 - , base >= 4.7 && < 5 - , bytestring - , http-types - , servant == 0.5.* - , servant-server == 0.5.* - , socket-io - , engine-io - , engine-io-wai - , text - , wai - , warp - , transformers - , stm - , mtl - ghc-options: -Wall -O2 -threaded - hs-source-dirs: socket-io-chat - default-language: Haskell2010 diff --git a/servant-examples/socket-io-chat/socket-io-chat.hs b/servant-examples/socket-io-chat/socket-io-chat.hs deleted file mode 100644 index 1250d8fe..00000000 --- a/servant-examples/socket-io-chat/socket-io-chat.hs +++ /dev/null @@ -1,54 +0,0 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeOperators #-} - - -import Data.Monoid ((<>)) -#if !MIN_VERSION_base(4,8,0) -import Control.Applicative ((<$>)) -#endif -import Network.EngineIO.Wai -import Network.Wai -import Network.Wai.Handler.Warp (run) -import Servant - - -import qualified Control.Concurrent.STM as STM -import qualified Network.SocketIO as SocketIO - - -import Chat (ServerState (..), eioServer) - - -type API = "socket.io" :> Raw - :<|> Raw - - -api :: Proxy API -api = Proxy - - -server :: WaiMonad () -> Server API -server sHandler = socketIOHandler - :<|> serveDirectory "socket-io-chat/resources" - - where - socketIOHandler req respond = toWaiApplication sHandler req respond - - -app :: WaiMonad () -> Application -app sHandler = serve api $ server sHandler - -port :: Int -port = 3001 - - -main :: IO () -main = do - state <- ServerState <$> STM.newTVarIO 0 - sHandler <- SocketIO.initialize waiAPI (eioServer state) - putStrLn $ "Running on " <> show port - run port $ app sHandler - - diff --git a/servant-examples/tutorial/T1.hs b/servant-examples/tutorial/T1.hs deleted file mode 100644 index 97bbecb8..00000000 --- a/servant-examples/tutorial/T1.hs +++ /dev/null @@ -1,45 +0,0 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} -module T1 where - -import Data.Aeson -import Data.Time.Calendar -import GHC.Generics -import Network.Wai -import Servant - -data User = User - { name :: String - , age :: Int - , email :: String - , registration_date :: Day - } deriving (Eq, Show, Generic) - -#if !MIN_VERSION_aeson(0,10,0) --- orphan ToJSON instance for Day. necessary to derive one for User -instance ToJSON Day where - -- display a day in YYYY-mm-dd format - toJSON d = toJSON (showGregorian d) -#endif - -instance ToJSON User - -type UserAPI = "users" :> Get '[JSON] [User] - -users :: [User] -users = - [ User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1) - , User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1) - ] - -userAPI :: Proxy UserAPI -userAPI = Proxy - -server :: Server UserAPI -server = return users - -app :: Application -app = serve userAPI server diff --git a/servant-examples/tutorial/T10.hs b/servant-examples/tutorial/T10.hs deleted file mode 100644 index be5da4cf..00000000 --- a/servant-examples/tutorial/T10.hs +++ /dev/null @@ -1,71 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} -module T10 where - -import Data.ByteString.Lazy (ByteString) -import Data.Text.Lazy (pack) -import Data.Text.Lazy.Encoding (encodeUtf8) -import Network.HTTP.Types -import Network.Wai -import Servant -import Servant.Docs -import qualified T3 - -type DocsAPI = T3.API :<|> Raw - -instance ToCapture (Capture "x" Int) where - toCapture _ = DocCapture "x" "(integer) position on the x axis" - -instance ToCapture (Capture "y" Int) where - toCapture _ = DocCapture "y" "(integer) position on the y axis" - -instance ToSample T3.Position where - toSamples _ = singleSample (T3.Position 3 14) - -instance ToParam (QueryParam "name" String) where - toParam _ = - DocQueryParam "name" - ["Alp", "John Doe", "..."] - "Name of the person to say hello to." - Normal - -instance ToSample T3.HelloMessage where - toSamples _ = - [ ("When a value is provided for 'name'", T3.HelloMessage "Hello, Alp") - , ("When 'name' is not specified", T3.HelloMessage "Hello, anonymous coward") - ] - -ci :: T3.ClientInfo -ci = T3.ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"] - -instance ToSample T3.ClientInfo where - toSamples _ = singleSample ci - -instance ToSample T3.Email where - toSamples _ = singleSample (T3.emailForClient ci) - -api :: Proxy DocsAPI -api = Proxy - -docsBS :: ByteString -docsBS = encodeUtf8 - . pack - . markdown - $ docsWithIntros [intro] T3.api - - where intro = DocIntro "Welcome" ["This is our super webservice's API.", "Enjoy!"] - -server :: Server DocsAPI -server = T3.server :<|> serveDocs - - where serveDocs _ respond = - respond $ responseLBS ok200 [plain] docsBS - - plain = ("Content-Type", "text/plain") - -app :: Application -app = serve api server diff --git a/servant-examples/tutorial/T2.hs b/servant-examples/tutorial/T2.hs deleted file mode 100644 index fc49d256..00000000 --- a/servant-examples/tutorial/T2.hs +++ /dev/null @@ -1,52 +0,0 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} -module T2 where - -import Data.Aeson -import Data.Time.Calendar -import GHC.Generics -import Network.Wai -import Servant - -data User = User - { name :: String - , age :: Int - , email :: String - , registration_date :: Day - } deriving (Eq, Show, Generic) - -#if !MIN_VERSION_aeson(0,10,0) --- orphan ToJSON instance for Day. necessary to derive one for User -instance ToJSON Day where - -- display a day in YYYY-mm-dd format - toJSON d = toJSON (showGregorian d) -#endif - -instance ToJSON User - -type UserAPI = "users" :> Get '[JSON] [User] - :<|> "albert" :> Get '[JSON] User - :<|> "isaac" :> Get '[JSON] User - -isaac :: User -isaac = User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1) - -albert :: User -albert = User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1) - -users :: [User] -users = [isaac, albert] - -userAPI :: Proxy UserAPI -userAPI = Proxy - -server :: Server UserAPI -server = return users - :<|> return albert - :<|> return isaac - -app :: Application -app = serve userAPI server diff --git a/servant-examples/tutorial/T3.hs b/servant-examples/tutorial/T3.hs deleted file mode 100644 index 7b5bdeb3..00000000 --- a/servant-examples/tutorial/T3.hs +++ /dev/null @@ -1,84 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} -module T3 where - -import Control.Monad.Trans.Except -import Data.Aeson -import Data.List -import GHC.Generics -import Network.Wai -import Servant - -data Position = Position - { x :: Int - , y :: Int - } deriving (Show, Generic) - -instance FromJSON Position -instance ToJSON Position - -newtype HelloMessage = HelloMessage { msg :: String } - deriving (Show, Generic) - -instance FromJSON HelloMessage -instance ToJSON HelloMessage - -data ClientInfo = ClientInfo - { name :: String - , email :: String - , age :: Int - , interested_in :: [String] - } deriving (Show, Generic) - -instance FromJSON ClientInfo -instance ToJSON ClientInfo - -data Email = Email - { from :: String - , to :: String - , subject :: String - , body :: String - } deriving (Show, Generic) - -instance FromJSON Email -instance ToJSON Email - -emailForClient :: ClientInfo -> Email -emailForClient c = Email from' to' subject' body' - - where from' = "great@company.com" - to' = email c - subject' = "Hey " ++ name c ++ ", we miss you!" - body' = "Hi " ++ name c ++ ",\n\n" - ++ "Since you've recently turned " ++ show (age c) - ++ ", have you checked out our latest " - ++ intercalate ", " (interested_in c) - ++ " products? Give us a visit!" - -type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position - :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage - :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email - -api :: Proxy API -api = Proxy - -server :: Server API -server = position - :<|> hello - :<|> marketing - - where position :: Int -> Int -> ExceptT ServantErr IO Position - position x y = return (Position x y) - - hello :: Maybe String -> ExceptT ServantErr IO HelloMessage - hello mname = return . HelloMessage $ case mname of - Nothing -> "Hello, anonymous coward" - Just n -> "Hello, " ++ n - - marketing :: ClientInfo -> ExceptT ServantErr IO Email - marketing clientinfo = return (emailForClient clientinfo) - -app :: Application -app = serve api server diff --git a/servant-examples/tutorial/T4.hs b/servant-examples/tutorial/T4.hs deleted file mode 100644 index 69cbf951..00000000 --- a/servant-examples/tutorial/T4.hs +++ /dev/null @@ -1,63 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} -module T4 where - -import Data.Aeson -import Data.Foldable (foldMap) -import GHC.Generics -import Lucid -import Network.Wai -import Servant -import Servant.HTML.Lucid - -data Person = Person - { firstName :: String - , lastName :: String - , age :: Int - } deriving Generic -- for the JSON instance - --- JSON serialization -instance ToJSON Person - --- HTML serialization of a single person -instance ToHtml Person where - toHtml person = - tr_ $ do - td_ (toHtml $ firstName person) - td_ (toHtml $ lastName person) - td_ (toHtml . show $ age person) - - toHtmlRaw = toHtml - --- HTML serialization of a list of persons -instance ToHtml [Person] where - toHtml persons = table_ $ do - tr_ $ do - th_ "first name" - th_ "last name" - th_ "age" - - foldMap toHtml persons - - toHtmlRaw = toHtml - -persons :: [Person] -persons = - [ Person "Isaac" "Newton" 372 - , Person "Albert" "Einstein" 136 - ] - -type PersonAPI = "persons" :> Get '[JSON, HTML] [Person] - -personAPI :: Proxy PersonAPI -personAPI = Proxy - -server :: Server PersonAPI -server = return persons - -app :: Application -app = serve personAPI server diff --git a/servant-examples/tutorial/T5.hs b/servant-examples/tutorial/T5.hs deleted file mode 100644 index 3b18aedb..00000000 --- a/servant-examples/tutorial/T5.hs +++ /dev/null @@ -1,37 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} -module T5 where - -import Control.Monad.IO.Class -import Control.Monad.Trans.Except -import Data.Aeson -import GHC.Generics -import Network.Wai -import Servant -import System.Directory - -type IOAPI = "myfile.txt" :> Get '[JSON] FileContent - -ioAPI :: Proxy IOAPI -ioAPI = Proxy - -newtype FileContent = FileContent - { content :: String } - deriving Generic - -instance ToJSON FileContent - -server :: Server IOAPI -server = do - exists <- liftIO (doesFileExist "myfile.txt") - if exists - then liftIO (readFile "myfile.txt") >>= return . FileContent - else throwE custom404Err - - where custom404Err = err404 { errBody = "myfile.txt just isn't there, please leave this server alone." } - -app :: Application -app = serve ioAPI server diff --git a/servant-examples/tutorial/T6.hs b/servant-examples/tutorial/T6.hs deleted file mode 100644 index 781bf703..00000000 --- a/servant-examples/tutorial/T6.hs +++ /dev/null @@ -1,18 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} -module T6 where - -import Network.Wai -import Servant - -type API = "code" :> Raw - -api :: Proxy API -api = Proxy - -server :: Server API -server = serveDirectory "tutorial" - -app :: Application -app = serve api server diff --git a/servant-examples/tutorial/T7.hs b/servant-examples/tutorial/T7.hs deleted file mode 100644 index e0145caf..00000000 --- a/servant-examples/tutorial/T7.hs +++ /dev/null @@ -1,33 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} -module T7 where - -import Control.Monad.Trans.Except -import Control.Monad.Trans.Reader -import Network.Wai -import Servant - -type ReaderAPI = "a" :> Get '[JSON] Int - :<|> "b" :> Get '[JSON] String - -readerAPI :: Proxy ReaderAPI -readerAPI = Proxy - -readerServerT :: ServerT ReaderAPI (Reader String) -readerServerT = a :<|> b - - where a :: Reader String Int - a = return 1797 - - b :: Reader String String - b = ask - -readerServer :: Server ReaderAPI -readerServer = enter readerToEither readerServerT - - where readerToEither :: Reader String :~> ExceptT ServantErr IO - readerToEither = Nat $ \r -> return (runReader r "hi") - -app :: Application -app = serve readerAPI readerServer diff --git a/servant-examples/tutorial/T9.hs b/servant-examples/tutorial/T9.hs deleted file mode 100644 index 75dd0630..00000000 --- a/servant-examples/tutorial/T9.hs +++ /dev/null @@ -1,105 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeOperators #-} -module T9 where - -import Control.Applicative -import Control.Monad.IO.Class -import Data.Aeson -import Data.Text (Text) -import GHC.Generics -import Network.Wai -import Servant -import Servant.JS -import System.Random - -import qualified Data.Text as T -import qualified Data.Text.IO as TIO -import qualified Language.Javascript.JQuery as JQ - -data Point = Point - { x :: Double - , y :: Double - } deriving Generic - -instance ToJSON Point - -randomPoint :: MonadIO m => m Point -randomPoint = liftIO . getStdRandom $ \g -> - let (rx, g') = randomR (-1, 1) g - (ry, g'') = randomR (-1, 1) g' - in (Point rx ry, g'') - -data Search a = Search - { query :: Text - , results :: [a] - } deriving Generic - -mkSearch :: Text -> [a] -> Search a -mkSearch = Search - -instance ToJSON a => ToJSON (Search a) - -data Book = Book - { author :: Text - , title :: Text - , year :: Int - } deriving Generic - -instance ToJSON Book - -book :: Text -> Text -> Int -> Book -book = Book - -books :: [Book] -books = - [ book "Paul Hudak" "The Haskell School of Expression: Learning Functional Programming through Multimedia" 2000 - , book "Bryan O'Sullivan, Don Stewart, and John Goerzen" "Real World Haskell" 2008 - , book "Miran Lipovača" "Learn You a Haskell for Great Good!" 2011 - , book "Graham Hutton" "Programming in Haskell" 2007 - , book "Simon Marlow" "Parallel and Concurrent Programming in Haskell" 2013 - , book "Richard Bird" "Introduction to Functional Programming using Haskell" 1998 - ] - -searchBook :: Monad m => Maybe Text -> m (Search Book) -searchBook Nothing = return (mkSearch "" books) -searchBook (Just q) = return (mkSearch q books') - - where books' = filter (\b -> q' `T.isInfixOf` T.toLower (author b) - || q' `T.isInfixOf` T.toLower (title b) - ) - books - q' = T.toLower q - -type API = "point" :> Get '[JSON] Point - :<|> "books" :> QueryParam "q" Text :> Get '[JSON] (Search Book) - -type API' = API :<|> Raw - -api :: Proxy API -api = Proxy - -api' :: Proxy API' -api' = Proxy - -server :: Server API -server = randomPoint - :<|> searchBook - -server' :: Server API' -server' = server - :<|> serveDirectory "tutorial/t9" - -apiJS :: Text -apiJS = jsForAPI api jquery - -writeJSFiles :: IO () -writeJSFiles = do - TIO.writeFile "tutorial/t9/api.js" apiJS - jq <- TIO.readFile =<< JQ.file - TIO.writeFile "tutorial/t9/jq.js" jq - -app :: Application -app = serve api' server' diff --git a/servant-examples/wai-middleware/wai-middleware.hs b/servant-examples/wai-middleware/wai-middleware.hs deleted file mode 100644 index a2e95860..00000000 --- a/servant-examples/wai-middleware/wai-middleware.hs +++ /dev/null @@ -1,51 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE OverloadedStrings #-} -import Data.Aeson -import Data.Text -import GHC.Generics -import Network.Wai -import Network.Wai.Handler.Warp -import Network.Wai.Middleware.RequestLogger -import Servant - -data Product = Product - { name :: Text - , brand :: Text - , current_price_eur :: Double - , available :: Bool - } deriving (Eq, Show, Generic) - -instance ToJSON Product - -products :: [Product] -products = [p1, p2] - - where p1 = Product "Haskell laptop sticker" - "GHC Industries" - 2.50 - True - - p2 = Product "Foldable USB drive" - "Well-Typed" - 13.99 - False - -type SimpleAPI = Get '[JSON] [Product] - -simpleAPI :: Proxy SimpleAPI -simpleAPI = Proxy - -server :: Server SimpleAPI -server = return products - --- logStdout :: Middleware --- i.e, logStdout :: Application -> Application --- serve :: Proxy api -> Context context -> Server api -> Application --- so applying a middleware is really as simple as --- applying a function to the result of 'serve' -app :: Application -app = logStdout (serve simpleAPI server) - -main :: IO () -main = run 8080 app From 050f071a0b13a402cf6159ab93aee1112824b405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Sat, 19 Mar 2016 17:16:21 +0800 Subject: [PATCH 49/50] tutorial: update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1ea6ffef..163de4bd 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ shell.nix default.nix doc/_build doc/venv +doc/tutorial/static/api.js +doc/tutorial/static/jq.js From 610b837e8da4d98dec0565d1a78b07b2aa538266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Hahn?= Date: Sat, 19 Mar 2016 17:28:39 +0800 Subject: [PATCH 50/50] tutorial: html tweak --- doc/tutorial/static/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/tutorial/static/index.html b/doc/tutorial/static/index.html index 6a047c1c..bfc55b59 100644 --- a/doc/tutorial/static/index.html +++ b/doc/tutorial/static/index.html @@ -3,7 +3,7 @@ - Tutorial - 9 - servant-jquery + servant-js Example

      Books