2435 lines
83 KiB
HTML
2435 lines
83 KiB
HTML
<!DOCTYPE html>
|
||
<html lang='en'>
|
||
|
||
<head>
|
||
<meta charset='UTF-8' />
|
||
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
||
<title>
|
||
Webapp-Development in Haskell – Home
|
||
</title>
|
||
|
||
|
||
<meta property='og:description' content='Step-by-Step-Anleitung, wie man ein neues Projekt mit einer bereits erprobten Pipeline erstellt.' />
|
||
<meta property='og:site_name' content='Home' />
|
||
<meta property='og:image' content />
|
||
<meta property='og:type' content='website' />
|
||
<meta property='og:title' content='Webapp-Development in Haskell' />
|
||
|
||
|
||
<base href='/' />
|
||
<link href='favicon.svg' rel='icon' />
|
||
|
||
<script>
|
||
window.MathJax = {
|
||
startup: {
|
||
ready: () => {
|
||
MathJax.startup.defaultReady();
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
<script async id='MathJax-script' src='https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js'></script>
|
||
|
||
<!-- mermaid.js --><script src='https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js'></script>
|
||
<script>
|
||
mermaid.initialize({startOnLoad:false});
|
||
mermaid.init(undefined,document.querySelectorAll(".mermaid"));
|
||
</script>
|
||
|
||
<!-- highlight.js -->
|
||
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/styles/hybrid.min.css' />
|
||
<script src='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js'></script>
|
||
<!-- Include languages that Emanote itself uses -->
|
||
<script src='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/languages/haskell.min.js'></script>
|
||
<script src='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/languages/nix.min.js'></script>
|
||
<script>hljs.highlightAll();</script>
|
||
|
||
|
||
|
||
|
||
<link href='tailwind.css?instanceId=491bb520-566c-4957-b60d-e90510adcf88' rel='stylesheet' type='text/css' />
|
||
|
||
<style>
|
||
/* Heist error element */
|
||
strong.error {
|
||
color: lightcoral;
|
||
font-size: 90%;
|
||
font-family: monospace;
|
||
}
|
||
|
||
/* External link icon */
|
||
a[data-linkicon=""]::after {
|
||
content: ""
|
||
}
|
||
|
||
a[data-linkicon=none]::after {
|
||
content: ""
|
||
}
|
||
|
||
a[data-linkicon="external"]::after {
|
||
content: url('data:image/svg+xml,\
|
||
<svg xmlns="http://www.w3.org/2000/svg" height="0.7em" viewBox="0 0 20 20"> \
|
||
<g style="stroke:gray;stroke-width:1"> \
|
||
<line x1="5" y1="5" x2="5" y2="14" /> \
|
||
<line x1="14" y1="9" x2="14" y2="14" /> \
|
||
<line x1="5" y1="14" x2="14" y2="14" /> \
|
||
<line x1="5" y1="5" x2="9" y2="5" /> \
|
||
<line x1="10" y1="2" x2="17" y2="2" /> \
|
||
<line x1="17" y1="2" x2="17" y2="9" /> \
|
||
<line x1="10" y1="9" x2="17" y2="2" style="stroke-width:1.0" /> \
|
||
</g> \
|
||
</svg>');
|
||
}
|
||
|
||
a[data-linkicon="external"][href^="mailto:"]::after {
|
||
content: url('data:image/svg+xml,\
|
||
<svg \
|
||
xmlns="http://www.w3.org/2000/svg" \
|
||
height="0.7em" \
|
||
fill="none" \
|
||
viewBox="0 0 24 24" \
|
||
stroke="gray" \
|
||
stroke-width="2"> \
|
||
<path \
|
||
stroke-linecap="round" \
|
||
stroke-linejoin="round" \
|
||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /> \
|
||
</svg>');
|
||
}
|
||
</style>
|
||
<!-- What goes in this file will appear on near the end of <head>--><link rel='preload' href='_emanote-static/fonts/Work_Sans/WorkSans-VariableFont_wght.ttf' as='font' type='font/ttf' crossorigin />
|
||
|
||
<style>
|
||
@font-face {
|
||
font-family: 'WorkSans';
|
||
/* FIXME: This ought to be: ${ema:emanoteStaticLayerUrl}/fonts/Work_Sans/WorkSans-VariableFont_wght.ttf */
|
||
src: url(_emanote-static/fonts/Work_Sans/WorkSans-VariableFont_wght.ttf) format("truetype");
|
||
font-display: swap;
|
||
}
|
||
|
||
body {
|
||
font-family: 'WorkSans', sans-serif;
|
||
font-variation-settings: 'wght' 350;
|
||
}
|
||
|
||
a.mavenLinkBold {
|
||
font-variation-settings: 'wght' 400;
|
||
}
|
||
|
||
strong {
|
||
font-variation-settings: 'wght' 500;
|
||
}
|
||
|
||
h1,
|
||
h2,
|
||
h3,
|
||
h4,
|
||
h5,
|
||
h6,
|
||
header,
|
||
.header-font {
|
||
font-family: 'WorkSans', sans-serif;
|
||
}
|
||
|
||
h1 {
|
||
font-variation-settings: 'wght' 500;
|
||
}
|
||
|
||
h2 {
|
||
font-variation-settings: 'wght' 400;
|
||
}
|
||
|
||
h3 {
|
||
font-variation-settings: 'wght' 300;
|
||
}
|
||
</style>
|
||
|
||
|
||
|
||
<link rel='stylesheet' href='_emanote-static/inverted-tree.css' />
|
||
|
||
<link rel='stylesheet' href='_emanote-static/stork/flat.css' />
|
||
<!-- Custom Stork-search styling for Emanote -->
|
||
<style>
|
||
#stork-search-container {
|
||
z-index: 1000;
|
||
background-color: rgb(15 23 42/.8);
|
||
}
|
||
|
||
.stork-overflow-hidden-important {
|
||
overflow: hidden !important;
|
||
}
|
||
</style>
|
||
|
||
|
||
<script src='_emanote-static/stork/stork.js'></script>
|
||
|
||
|
||
<script id='emanote-stork' data-emanote-base-url='/'>
|
||
window.emanote = {};
|
||
window.emanote.stork = {
|
||
searchShown: false,
|
||
indexIsStale: false,
|
||
toggleSearch: function () {
|
||
window.emanote.stork.refreshIndex();
|
||
document.getElementById('stork-search-container').classList.toggle('hidden');
|
||
window.emanote.stork.searchShown = document.body.classList.toggle('stork-overflow-hidden-important');
|
||
if (window.emanote.stork.searchShown) {
|
||
document.getElementById('stork-search-input').focus();
|
||
}
|
||
},
|
||
clearSearch: function () {
|
||
document.getElementById('stork-search-container').classList.add('hidden');
|
||
document.body.classList.remove('stork-overflow-hidden-important');
|
||
window.emanote.stork.searchShown = false;
|
||
},
|
||
|
||
getBaseUrl: function () {
|
||
const baseUrl = document.getElementById("emanote-stork").getAttribute('data-emanote-base-url') || '/';
|
||
return baseUrl;
|
||
},
|
||
|
||
registerIndex: function (options) {
|
||
const indexName = 'emanote-search'; // used to match input[data-stork] attribute value
|
||
const indexUrl = window.emanote.stork.getBaseUrl() + '-/stork.st';
|
||
stork.register(
|
||
indexName,
|
||
indexUrl,
|
||
options);
|
||
},
|
||
|
||
init: function () {
|
||
if (document.readyState !== 'complete') {
|
||
window.addEventListener('load', function () {
|
||
stork.initialize(window.emanote.stork.getBaseUrl() + '_emanote-static/stork/stork.wasm');
|
||
window.emanote.stork.registerIndex();
|
||
});
|
||
|
||
document.addEventListener('keydown', event => {
|
||
if (window.emanote.stork.searchShown && event.key === 'Escape') {
|
||
window.emanote.stork.clearSearch();
|
||
event.preventDefault();
|
||
} else if ((event.key == 'k' || event.key == 'K') && (event.ctrlKey || event.metaKey)) {
|
||
window.emanote.stork.toggleSearch();
|
||
event.preventDefault();
|
||
}
|
||
});
|
||
} else {
|
||
// This section is called during Ema's hot reload.
|
||
//
|
||
// Mark the current index as stale, and refresh it *only when* the
|
||
// user actually invokes search.
|
||
//
|
||
// We do not refresh the index *right away*, as that will cause
|
||
// memory leaks in the browser. See
|
||
// https://github.com/srid/emanote/issues/411#issuecomment-1402056235
|
||
console.log("stork: Marking index as stale");
|
||
window.emanote.stork.markIndexAsStale();
|
||
}
|
||
},
|
||
|
||
markIndexAsStale: function () {
|
||
window.emanote.stork.indexIsStale = true;
|
||
},
|
||
|
||
refreshIndex: function () {
|
||
if (window.emanote.stork.indexIsStale) {
|
||
console.log("stork: Reloading index");
|
||
window.emanote.stork.indexIsStale = false;
|
||
// NOTE: This will leak memory. See the comment above.
|
||
window.emanote.stork.registerIndex({ forceOverwrite: true });
|
||
}
|
||
}
|
||
|
||
};
|
||
|
||
window.emanote.stork.init();
|
||
</script>
|
||
|
||
|
||
</head>
|
||
|
||
<!-- DoNotFormat -->
|
||
|
||
|
||
|
||
<!-- DoNotFormat -->
|
||
|
||
<body class='bg-gray-400 overflow-y-scroll'>
|
||
|
||
<div class='container mx-auto'>
|
||
|
||
<nav id='breadcrumbs' class='w-full text-gray-700 md:hidden'>
|
||
<div class='flex justify-left'>
|
||
<div class='w-full px-2 py-2 bg-gray-50'>
|
||
<ul class='flex flex-wrap text-lg'>
|
||
<li class='inline-flex items-center'>
|
||
|
||
|
||
<img style='width: 1rem;' src='favicon.svg' />
|
||
|
||
|
||
</li>
|
||
|
||
|
||
<li class='inline-flex items-center'>
|
||
<a class='px-1 font-bold' href=''>
|
||
Home
|
||
</a>
|
||
<svg fill='currentColor' viewBox='0 0 20 20' class='w-auto h-5 text-gray-400'>
|
||
<path fill-rule='evenodd' d='M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z' clip-rule='evenodd'></path>
|
||
</svg>
|
||
</li>
|
||
|
||
<li class='inline-flex items-center'>
|
||
<a class='px-1 font-bold' href='Coding'>
|
||
Coding
|
||
</a>
|
||
<svg fill='currentColor' viewBox='0 0 20 20' class='w-auto h-5 text-gray-400'>
|
||
<path fill-rule='evenodd' d='M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z' clip-rule='evenodd'></path>
|
||
</svg>
|
||
</li>
|
||
|
||
<li class='inline-flex items-center'>
|
||
<a class='px-1 font-bold' href='Coding/Haskell'>
|
||
Haskell
|
||
</a>
|
||
<svg fill='currentColor' viewBox='0 0 20 20' class='w-auto h-5 text-gray-400'>
|
||
<path fill-rule='evenodd' d='M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z' clip-rule='evenodd'></path>
|
||
</svg>
|
||
</li>
|
||
|
||
|
||
</ul>
|
||
</div>
|
||
<button class='inline px-2 py-1 bg-gray-50 outline-none cursor-pointer focus:outline-none' title='Search (Ctrl+K)' type='button' onclick='window.emanote.stork.toggleSearch()'>
|
||
<svg xmlns='http://www.w3.org/2000/svg' style='width: 1rem;' class='hover:text-purple-700' f
|
||
fill='none' viewBox='0 0 24 24' stroke='currentColor' stroke-width='2'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'></path>
|
||
</svg>
|
||
</button>
|
||
<button class='inline px-2 py-1 text-white bg-purple-600 outline-none cursor-pointer focus:outline-none' title='Toggle sidebar' type='button' onclick="toggleHidden('sidebar')">
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 6h16M4 12h16M4 18h16'></path>
|
||
</svg>
|
||
</button>
|
||
<script>
|
||
function toggleHidden(elemId) {
|
||
document.getElementById(elemId).classList.toggle("hidden");
|
||
}
|
||
</script>
|
||
</div>
|
||
</nav>
|
||
|
||
<div id='container' class='flex flex-nowrap flex-col md:flex-row bg-gray-50 md:mt-8 md:shadow-2xl md:mb-8'>
|
||
<!-- Sidebar column -->
|
||
<nav id='sidebar' class='flex-shrink hidden leading-relaxed md:block md:sticky md:top-0 md:h-full md:w-48 xl:w-64'>
|
||
<div class='px-2 py-2 text-gray-800'>
|
||
<div id='indexing-links' class='flex flex-row float-right p-2 space-x-2 text-gray-500'>
|
||
<a href='-/tags' title='View tags'>
|
||
<svg style='width: 1rem;' class='hover:text-purple-700' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z'>
|
||
</path>
|
||
</svg>
|
||
</a>
|
||
<a href='-/all' title='Expand full tree'>
|
||
<svg style='width: 1rem;' class='hover:text-purple-700' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4'>
|
||
</path>
|
||
</svg>
|
||
</a>
|
||
<a title='Search (Ctrl+K)' class='cursor-pointer' onclick='window.emanote.stork.toggleSearch()'>
|
||
<svg xmlns='http://www.w3.org/2000/svg' style='width: 1rem;' class='hover:text-purple-700' f
|
||
fill='none' viewBox='0 0 24 24' stroke='currentColor' stroke-width='2'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'></path>
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
|
||
<div id='site-logo' class='pl-2'>
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
<a href='' title='Go to Home'>
|
||
|
||
|
||
<!-- The style width attribute here is to prevent huge
|
||
icon from displaying at those rare occasions when Tailwind
|
||
hasn't kicked in immediately on page load
|
||
-->
|
||
<img style='width: 1rem;' class='transition transform hover:scale-110 hover:opacity-80' src='favicon.svg' />
|
||
|
||
|
||
</a>
|
||
<a class='font-bold truncate' title='Go to Home' href=''>
|
||
Home
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-4 h-4 flex-shrink-0 inline text-gray-700' viewBox='0 0 20 20' fill='currentColor'>
|
||
<path fill-rule='evenodd' d='M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z' clip-rule='evenodd'></path>
|
||
<path d='M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z'></path>
|
||
</svg>
|
||
|
||
|
||
<a class='font-bold hover:underline truncate' title='About me - Drezil' href='About'>
|
||
About me - Drezil
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Curriculum Vitae' href='About/CV'>
|
||
Curriculum Vitae
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Highlights of my experiences in the programming world' href='About/Experience'>
|
||
Highlights of my experiences in the programming world
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Work-Experience' href='About/Work'>
|
||
Work-Experience
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-4 h-4 flex-shrink-0 inline text-gray-700' viewBox='0 0 20 20' fill='currentColor'>
|
||
<path fill-rule='evenodd' d='M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z' clip-rule='evenodd'></path>
|
||
<path d='M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z'></path>
|
||
</svg>
|
||
|
||
|
||
<a class='font-bold hover:underline truncate' title='Android' href='Android'>
|
||
Android
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Einrichtung Android-Smartphones' href='Android/Einrichtung'>
|
||
Einrichtung Android-Smartphones
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-4 h-4 flex-shrink-0 inline text-gray-700' viewBox='0 0 20 20' fill='currentColor'>
|
||
<path fill-rule='evenodd' d='M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z' clip-rule='evenodd'></path>
|
||
<path d='M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z'></path>
|
||
</svg>
|
||
|
||
|
||
<a class='font-bold hover:underline truncate' title='Coding' href='Coding'>
|
||
Coding
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-4 h-4 flex-shrink-0 inline text-gray-700' viewBox='0 0 20 20' fill='currentColor'>
|
||
<path fill-rule='evenodd' d='M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z' clip-rule='evenodd'></path>
|
||
<path d='M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z'></path>
|
||
</svg>
|
||
|
||
|
||
<a class='font-bold hover:underline truncate' title='Haskell' href='Coding/Haskell'>
|
||
Haskell
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Talks und Posts zu Haskell' href='Coding/Haskell/Advantages'>
|
||
Talks und Posts zu Haskell
|
||
</a>
|
||
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-4 h-4 flex-shrink-0 inline text-gray-500' viewBox='0 0 20 20' fill='currentColor'>
|
||
<path d='M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z'></path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Code-Snippets' href='Coding/Haskell/Code%20Snippets'>
|
||
Code-Snippets
|
||
</a>
|
||
|
||
|
||
<span class='text-gray-300' title='2 children inside'>
|
||
2
|
||
</span>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Fortgeschrittene funktionale Programmierung in Haskell' href='Coding/Haskell/FFPiH'>
|
||
Fortgeschrittene funktionale Programmierung in Haskell
|
||
</a>
|
||
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Lenses' href='Coding/Haskell/Lenses'>
|
||
Lenses
|
||
</a>
|
||
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-4 h-4 flex-shrink-0 inline text-gray-700' viewBox='0 0 20 20' fill='currentColor'>
|
||
<path fill-rule='evenodd' d='M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z' clip-rule='evenodd'></path>
|
||
<path d='M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z'></path>
|
||
</svg>
|
||
|
||
|
||
<a class='font-bold text-purple-600 hover:underline truncate' title='Webapp-Development in Haskell' href='Coding/Haskell/Webapp-Example'>
|
||
Webapp-Development in Haskell
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Webapp-Example: Main.hs' href='Coding/Haskell/Webapp-Example/Main.hs'>
|
||
Webapp-Example: Main.hs
|
||
</a>
|
||
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Webapp-Example: MyService/Types.hs' href='Coding/Haskell/Webapp-Example/MyService_Types.hs'>
|
||
Webapp-Example: MyService/Types.hs
|
||
</a>
|
||
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Openapi-generator' href='Coding/OpenAPI'>
|
||
Openapi-generator
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-4 h-4 flex-shrink-0 inline text-gray-700' viewBox='0 0 20 20' fill='currentColor'>
|
||
<path fill-rule='evenodd' d='M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z' clip-rule='evenodd'></path>
|
||
<path d='M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z'></path>
|
||
</svg>
|
||
|
||
|
||
<a class='font-bold hover:underline truncate' title='Health' href='Health'>
|
||
Health
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Mental Health' href='Health/Issues'>
|
||
Mental Health
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Logik für Dummies' href='Logik'>
|
||
Logik für Dummies
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-4 h-4 flex-shrink-0 inline text-gray-700' viewBox='0 0 20 20' fill='currentColor'>
|
||
<path fill-rule='evenodd' d='M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z' clip-rule='evenodd'></path>
|
||
<path d='M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z'></path>
|
||
</svg>
|
||
|
||
|
||
<a class='font-bold hover:underline truncate' title='Opinions' href='Opinions'>
|
||
Opinions
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Editors' href='Opinions/Editors'>
|
||
Editors
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Keyboard-Layouts' href='Opinions/Layout'>
|
||
Keyboard-Layouts
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-4 h-4 flex-shrink-0 inline text-gray-700' viewBox='0 0 20 20' fill='currentColor'>
|
||
<path fill-rule='evenodd' d='M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z' clip-rule='evenodd'></path>
|
||
<path d='M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z'></path>
|
||
</svg>
|
||
|
||
|
||
<a class='font-bold hover:underline truncate' title='Stuff' href='Stuff'>
|
||
Stuff
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Die Bielefeld-Verschwörung' href='Stuff/Bielefeldverschwoerung'>
|
||
Die Bielefeld-Verschwörung
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Todo' href='TODO'>
|
||
Todo
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-4 h-4 flex-shrink-0 inline text-gray-700' viewBox='0 0 20 20' fill='currentColor'>
|
||
<path fill-rule='evenodd' d='M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z' clip-rule='evenodd'></path>
|
||
<path d='M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z'></path>
|
||
</svg>
|
||
|
||
|
||
<a class='font-bold hover:underline truncate' title='Uni' href='Uni'>
|
||
Uni
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Studium generale / University-Life' href='Uni/Extracurricular'>
|
||
Studium generale / University-Life
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='Wie lerne ich richtig an der Uni?' href='Uni/Lernerfolg_an_der_Uni'>
|
||
Wie lerne ich richtig an der Uni?
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-4 h-4 flex-shrink-0 inline text-gray-700' viewBox='0 0 20 20' fill='currentColor'>
|
||
<path fill-rule='evenodd' d='M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z' clip-rule='evenodd'></path>
|
||
<path d='M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z'></path>
|
||
</svg>
|
||
|
||
|
||
<a class='font-bold hover:underline truncate' title='Unix' href='Unix'>
|
||
Unix
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
<!-- Variable bindings for this tree-->
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<!-- Rendering of this tree -->
|
||
<div class='pl-2'>
|
||
<!-- Node's rootLabel-->
|
||
<div class='flex items-center my-2 space-x-2 justify-left'>
|
||
|
||
|
||
<svg class='w-4 h-4 flex-shrink-0 inline' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'>
|
||
</path>
|
||
</svg>
|
||
|
||
|
||
<a class='hover:underline truncate' title='SSH-Filter' href='Unix/SSH-Filter'>
|
||
SSH-Filter
|
||
</a>
|
||
|
||
|
||
</div>
|
||
|
||
<!-- Node's children forest, displayed only on active trees
|
||
TODO: Use <details> to toggle visibility?
|
||
-->
|
||
|
||
|
||
|
||
</div>
|
||
|
||
|
||
</div>
|
||
|
||
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Main body column -->
|
||
<div class='flex-1 w-full overflow-x-auto bg-white'>
|
||
<main class='px-4 py-4'>
|
||
<!-- DoNotFormat -->
|
||
<!-- DoNotFormat -->
|
||
|
||
<nav id='uptree' class='flipped tree' style='transform-origin: 50%;'>
|
||
<ul class='root'>
|
||
<li>
|
||
|
||
<ul>
|
||
|
||
<li>
|
||
|
||
<div class='text-gray-900 forest-link'>
|
||
<a href='Coding/OpenAPI'>
|
||
Openapi-generator
|
||
</a>
|
||
</div>
|
||
|
||
|
||
<ul>
|
||
|
||
<li>
|
||
|
||
<div class='text-gray-900 forest-link'>
|
||
<a href='Coding'>
|
||
Coding
|
||
</a>
|
||
</div>
|
||
|
||
|
||
<ul>
|
||
|
||
<li>
|
||
|
||
<div class='text-gray-900 forest-link'>
|
||
<a href=''>
|
||
Home
|
||
</a>
|
||
</div>
|
||
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
<div class='text-gray-900 forest-link'>
|
||
<a href='Coding/Haskell'>
|
||
Haskell
|
||
</a>
|
||
</div>
|
||
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
<h1 class='flex items-end justify-center mb-4 p-3 bg-purple-100 text-5xl font-extrabold text-black rounded'>
|
||
<a class='z-40 tracking-tighter '>
|
||
Webapp-Development in Haskell
|
||
</a>
|
||
</h1>
|
||
<article class='overflow-auto'>
|
||
<!-- What goes in this file will appear on top of note body-->
|
||
|
||
<p class='mb-3'>
|
||
Step-by-Step-Anleitung, wie man ein neues Projekt mit einer bereits erprobten Pipeline erstellt.
|
||
</p>
|
||
<h2 id='definition-der-api' class='mt-6 mb-4 text-4xl font-bold text-gray-700 border-b-2'>Definition der API</h2>
|
||
<p class='mb-3'>
|
||
Erster Schritt ist immer ein wünsch-dir-was bei der Api-Defenition.
|
||
</p>
|
||
|
||
<p class='mb-3'>
|
||
Die meisten Services haben offensichtliche Anforderungen (Schnittstellen nach draußen, Schnittstellen intern, …). Diese kann man immer sehr gut in einem <code class='py-0.5 px-0.5 bg-gray-100'>Request -> Response</code>-Model erfassen.
|
||
</p>
|
||
|
||
<p class='mb-3'>
|
||
Um die Anforderungen und Möglichkeiten des jeweiligen Services sauber zu erfassen und automatisiert zu prüfen, dummy-implementationen zu bekommen und vieles andere mehr, empfiehlt es sich den <a href='Coding/OpenAPI' class='text-purple-600 mavenLinkBold hover:underline' data-wikilink-type='WikiLinkTag'>openapi-generator</a> zu nutzen.
|
||
</p>
|
||
|
||
<p class='mb-3'>
|
||
Diese Definition läuft über openapi-v3 und kann z.b. mit Echtzeit-Vorschau im <a href='http://editor.swagger.io/' class='text-purple-600 hover:underline' data-linkicon='external' target='_blank' rel='noopener'>http://editor.swagger.io/</a> erspielen. Per Default ist der noch auf openapi-v2 (aka swagger), kann aber auch v3.
|
||
</p>
|
||
|
||
<p class='mb-3'>
|
||
Nach der Definition, was man am Ende haben möchte, muss man sich entscheiden, in welcher Sprache man weiter entwickelt. Ich empfehle aus verschiedenen Gründen primär 2 Sprachen: Python-Microservices (weil die ML-Libraries sehr gut sind, allerdings Änderungen meist schwer sind und der Code wenig robust - meist nur 1 API-Endpunkt pro service) und Haskell (Stabilität, Performace, leicht zu ändern, gut anzupassen).
|
||
</p>
|
||
|
||
<p class='mb-3'>
|
||
Im folgenden wird (aus offensichtlichen Gründen) nur auf das Haskell-Projekt eingegangen.
|
||
</p>
|
||
<h2 id='startprojekt-in-haskell' class='mt-6 mb-4 text-4xl font-bold text-gray-700 border-b-2'>Startprojekt in Haskell</h2><h3 id='erstellen-eines-neuen-projektes' class='mt-6 mb-2 text-3xl font-bold text-gray-700'>Erstellen eines neuen Projektes</h3>
|
||
<p class='mb-3'>
|
||
Zunächst erstellen wir in normales Haskell-Projekt ohne Funktionalität & Firlefanz:
|
||
</p>
|
||
<div class='py-0.5 mb-3 text-sm'><pre><code class='bash language-bash'>stack new myservice</code></pre></div>
|
||
<p class='mb-3'>
|
||
Dies erstellt ein neues Verzeichnis und das generelle scaffolding. Nach einer kurzen Anpassung der <code class='py-0.5 px-0.5 bg-gray-100'>stack.yaml</code> (resolver auf unserer setzen; aktuell: <code class='py-0.5 px-0.5 bg-gray-100'>lts-17.4</code>) fügen wir am Ende der Datei
|
||
</p>
|
||
<div class='py-0.5 mb-3 text-sm'><pre><code class='yaml language-yaml'>allow-newer: true
|
||
ghc-options:
|
||
"$locals": -fwrite-ide-info</code></pre></div>
|
||
<p class='mb-3'>
|
||
ein. Anschließend organisieren™ wir uns noch eine gute <code class='py-0.5 px-0.5 bg-gray-100'>.gitignore</code> und initialisieren das git mittels <code class='py-0.5 px-0.5 bg-gray-100'>git init; git add .; git commit -m "initial scaffold"</code>
|
||
</p>
|
||
<h3 id='generierung-der-api' class='mt-6 mb-2 text-3xl font-bold text-gray-700'>Generierung der API</h3>
|
||
<p class='mb-3'>
|
||
Da die API immer wieder neu generiert werden kann (und sollte!) liegt sich in einem unterverzeichnis des Hauptprojektes.
|
||
</p>
|
||
|
||
<p class='mb-3'>
|
||
Initial ist es das einfachste ein leeres temporäres Verzeichnis woanders zu erstellen, die <code class='py-0.5 px-0.5 bg-gray-100'>api-doc.yml</code> hinein kopieren und folgendes ausführen:
|
||
</p>
|
||
<div class='py-0.5 mb-3 text-sm'><pre><code class='bash language-bash'>openapi-generator generate -g haskell -o . -i api-doc.yml</code></pre></div>
|
||
<p class='mb-3'>
|
||
Dieses erstellt einem dann eine komplette library inkl. Datentypen. Wichtig: Der Name in der <code class='py-0.5 px-0.5 bg-gray-100'>api-doc</code> sollte vom Namen des Services (oben <code class='py-0.5 px-0.5 bg-gray-100'>myservice</code>) abweichen - entweder in Casing oder im Namen direkt. Suffixe wie API schneidet der Generator hier leider ab. (Wieso das ganze? Es entstehen nachher 2 libraries, <code class='py-0.5 px-0.5 bg-gray-100'>foo</code> & <code class='py-0.5 px-0.5 bg-gray-100'>fooAPI</code>. Da der generator das API abschneidet endet man mit foo & foo und der compiler meckert, dass er nicht weiß, welche lib gemeint ist).
|
||
</p>
|
||
|
||
<p class='mb-3'>
|
||
danach: wie gewohnt <code class='py-0.5 px-0.5 bg-gray-100'>git init; git add .; git commit -m "initial"</code>. Auf dem Server der Wahl (github, gitea, gitlab, …) nun ein Repository erstellen (am Besten: <code class='py-0.5 px-0.5 bg-gray-100'>myserviceAPI</code> - nach Konvention ist alles auf API endend autogeneriert!) und den Anweisungen nach ein remote hinzufügen & pushen.
|
||
</p>
|
||
<h4 id='wieder-zurück-im-haskell-service' class='mt-6 mb-2 text-2xl font-bold text-gray-700'>Wieder zurück im Haskell-Service</h4>
|
||
<p class='mb-3'>
|
||
In unserem eigentlichen Service müssen wir nun die API einbinden. Dazu erstellen wir ein Verzeichnis <code class='py-0.5 px-0.5 bg-gray-100'>libs</code> (Konvention) und machen ein <code class='py-0.5 px-0.5 bg-gray-100'>git submodule add <repository-url> libs/myserviceAPI</code>
|
||
</p>
|
||
|
||
<p class='mb-3'>
|
||
Git hat nun die API in das submodul gepackt und wir können das oben erstellte temporäre Verzeichnis wieder löschen.
|
||
</p>
|
||
|
||
<p class='mb-3'>
|
||
Anschließend müssen wir stack noch erklären, dass wir die API da nun liegen haben und passen wieder die <code class='py-0.5 px-0.5 bg-gray-100'>stack.yaml</code> an, indem wir das Verzeichnis unter packages hinzufügen.
|
||
</p>
|
||
<div class='py-0.5 mb-3 text-sm'><pre><code class='yaml language-yaml'>packages:
|
||
- .
|
||
- libs/myserviceAPI # <<</code></pre></div>
|
||
<p class='mb-3'>
|
||
Nun können wir in der <code class='py-0.5 px-0.5 bg-gray-100'>package.yaml</code> (oder <code class='py-0.5 px-0.5 bg-gray-100'>myservice.cabal</code>, falls kein <code class='py-0.5 px-0.5 bg-gray-100'>hpack</code> verwendet wird) unter den dependencies unsere API hinzufügen (name wie die cabal-Datei in <code class='py-0.5 px-0.5 bg-gray-100'>libs/myserviceAPI</code>).
|
||
</p>
|
||
<h3 id='einbinden-anderer-microservices' class='mt-6 mb-2 text-3xl font-bold text-gray-700'>Einbinden anderer Microservices</h3>
|
||
<p class='mb-3'>
|
||
Funktioniert komplett analog zu dem vorgehen oben (ohne das generieren natürlich <span class='emoji' data-emoji='grin' style='font-family: emoji'>😁</span>). <code class='py-0.5 px-0.5 bg-gray-100'>stack.yaml</code> editieren und zu den packages hinzufügen:
|
||
</p>
|
||
<div class='py-0.5 mb-3 text-sm'><pre><code class='yaml language-yaml'>packages:
|
||
- .
|
||
- libs/myserviceAPI
|
||
- libs/myCoolMLServiceAPI</code></pre></div>
|
||
<p class='mb-3'>
|
||
in der <code class='py-0.5 px-0.5 bg-gray-100'>package.yaml</code> (oder der cabal) die dependencies hinzufügen und schon haben wir die Features zur Verfügung und können gegen diese Services reden.
|
||
</p>
|
||
<h3 id='entfernen-von-anderen-technologienmicroservices' class='mt-6 mb-2 text-3xl font-bold text-gray-700'>Entfernen von anderen Technologien/Microservices</h3>
|
||
<p class='mb-3'>
|
||
In git ist das entfernen von Submodules etwas frickelig, daher hier ein copy&paste der <a href='https://gist.github.com/myusuf3/7f645819ded92bda6677' class='text-purple-600 hover:underline' data-linkicon='external' target='_blank' rel='noopener'>GitHub-Antwort</a>:
|
||
</p>
|
||
<div class='py-0.5 mb-3 text-sm'><pre><code class='bash language-bash'>## Remove the submodule entry from .git/config
|
||
git submodule deinit -f path/to/submodule
|
||
|
||
## Remove the submodule directory from the superproject's .git/modules directory
|
||
rm-rf .git/modules/path/to/submodule
|
||
|
||
## Remove the entry in .gitmodules and remove the submodule directory located at path/to/submodule
|
||
git rm-f path/to/submodule</code></pre></div>
|
||
<p class='mb-3'>
|
||
Falls das nicht klappt, gibt es alternative Vorschläge unter dem Link oben.
|
||
</p>
|
||
<h3 id='woher-weiss-ich-was-wo-liegt-dokumentation-halloo' class='mt-6 mb-2 text-3xl font-bold text-gray-700'>Woher weiss ich, was wo liegt? Dokumentation? Halloo??</h3>
|
||
<p class='mb-3'>
|
||
Keine Panik. Ein <code class='py-0.5 px-0.5 bg-gray-100'>stack haddock --open</code> hilft da. Das generiert die Dokumentation für alle in der <code class='py-0.5 px-0.5 bg-gray-100'>package.yaml</code> (oder cabal-file) eingetragenen dependencies inkl. aller upstream-dependencies. Man bekommt also eine komplette lokale Dokumentation von allem. Geöffnet wird dann die Paket-Startseite inkl. der direkten dependencies:
|
||
</p>
|
||
|
||
<p class='mb-3'>
|
||
Es gibt 2 wichtige Pfade im Browser:
|
||
</p>
|
||
|
||
<ul class='my-3 ml-6 space-y-1 list-disc'>
|
||
|
||
<li>
|
||
…../all/index.html - hier sind alle Pakete aufgeführt
|
||
</li>
|
||
|
||
<li>
|
||
…../index.html - hier sind nur die direkten dependencies aufgeführt.
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
<p class='mb-3'>
|
||
Wenn man einen lokalen Webserver startet kann man mittels “s” auch die interaktive Suche öffnen (Suche nach Typen, Funktionen, Signaturen, etc.). In Bash mit <code class='py-0.5 px-0.5 bg-gray-100'>python3</code> geht das z.b. einfach über:
|
||
</p>
|
||
<div class='py-0.5 mb-3 text-sm'><pre><code class='bash language-bash'>cd $(stack path --local-doc-root)
|
||
python3 -m SimpleHTTPServer 8000
|
||
firefox "http://localhost:8000"</code></pre></div><h3 id='implementation-des-services-und-start' class='mt-6 mb-2 text-3xl font-bold text-gray-700'>Implementation des Services und Start</h3><h4 id='loaderbootstrapper' class='mt-6 mb-2 text-2xl font-bold text-gray-700'>Loader/Bootstrapper</h4>
|
||
<p class='mb-3'>
|
||
Generelles Vorgehen:
|
||
</p>
|
||
|
||
<ul class='my-3 ml-6 space-y-1 list-disc'>
|
||
|
||
<li>
|
||
|
||
<p class='mb-3'>
|
||
in <code class='py-0.5 px-0.5 bg-gray-100'>app/Main.hs</code>: Hier ist quasi immer nur eine Zeile drin: <code class='py-0.5 px-0.5 bg-gray-100'>main = myServiceMain</code>
|
||
</p>
|
||
|
||
<p class='mb-3'>
|
||
Grund: Applications tauchen nicht im Haddock auf. Also haben wir ein “src”-Modul, welches hier nur geladen & ausgeführt wird.
|
||
</p>
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
<p class='mb-3'>
|
||
in <code class='py-0.5 px-0.5 bg-gray-100'>src/MyService.hs</code>: <code class='py-0.5 px-0.5 bg-gray-100'>myServiceMain :: IO ()</code> definieren
|
||
</p>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
<p class='mb-3'>
|
||
Für die Main kann man prinzipiell eine Main andere Services copy/pasten. Im folgenden eine Annotierte main-Funktion - zu den einzelnen Voraussetzungen kommen wir im Anschluss.
|
||
</p>
|
||
<section title='Embedded note' class='p-4 mx-2 mb-2 bg-white border-2 rounded-lg shadow-inner'>
|
||
<details>
|
||
<summary class='flex items-center justify-center text-2xl italic bg-purple-50 rounded py-1 px-2 mb-3'>
|
||
<header style='display:list-item'>
|
||
<a href='Coding/Haskell/Webapp-Example/Main.hs'>
|
||
Webapp-Example: Main.hs
|
||
</a>
|
||
</header>
|
||
</summary>
|
||
<div>
|
||
|
||
<p class='mb-3'>
|
||
Wie man das verwendet, siehe <a href='Coding/Haskell/Webapp-Example' class='text-purple-600 mavenLinkBold hover:underline' data-wikilink-type='WikiLinkTag'>Webapp-Development in Haskell</a>.
|
||
</p>
|
||
<div class='py-0.5 mb-3 text-sm'><pre><code class='haskell language-haskell'>{-# OPTIONS_GHC -Wno-name-shadowing #-}
|
||
{-# LANGUAGE FlexibleContexts #-}
|
||
{-# LANGUAGE LambdaCase #-}
|
||
{-# LANGUAGE OverloadedStrings #-}
|
||
{-# LANGUAGE RankNTypes #-}
|
||
{-# LANGUAGE RecordWildCards #-}
|
||
{-# LANGUAGE ScopedTypeVariables #-}
|
||
module MyService where
|
||
|
||
-- generische imports aus den dependencies/base, nicht in der prelude
|
||
import Codec.MIME.Type
|
||
import Configuration.Dotenv as Dotenv
|
||
import Control.Concurrent (forkIO, threadDelay)
|
||
import Control.Concurrent.Async
|
||
import Control.Concurrent.STM
|
||
import Control.Monad
|
||
import Control.Monad.Catch
|
||
import Control.Monad.Except
|
||
import Conversion
|
||
import Conversion.Text ()
|
||
import Data.Binary.Builder
|
||
import Data.String (IsString (..))
|
||
import Data.Time
|
||
import Data.Time.Clock
|
||
import Data.Time.Format
|
||
import Data.Default
|
||
import Network.HostName
|
||
import Network.HTTP.Client as HTTP hiding
|
||
(withConnection)
|
||
import Network.HTTP.Types (Status, statusCode)
|
||
import Network.Mom.Stompl.Client.Queue
|
||
import Network.Wai (Middleware)
|
||
import Network.Wai.Logger
|
||
import Network.Wai.Middleware.Cors
|
||
import Network.Wai.Middleware.RequestLogger (OutputFormat (..),
|
||
logStdout,
|
||
mkRequestLogger,
|
||
outputFormat)
|
||
import Servant.Client (mkClientEnv,
|
||
parseBaseUrl)
|
||
import System.Directory
|
||
import System.Envy
|
||
import System.IO
|
||
import System.Log.FastLogger
|
||
import Text.PrettyPrint.GenericPretty
|
||
|
||
-- generische imports, aber qualified, weil es sonst zu name-clashes kommt
|
||
|
||
import qualified Data.ByteString as BS
|
||
-- import qualified Data.ByteString.Char8 as BS8
|
||
import qualified Data.ByteString.Lazy as LBS
|
||
import qualified Network.HTTP.Client.TLS as UseDefaultHTTPSSettings (tlsManagerSettings)
|
||
import qualified Network.Mom.Stompl.Client.Queue as AMQ
|
||
import qualified Network.Wai as WAI
|
||
|
||
-- Handler für den MyServiceBackend-Typen und Imports aus den Libraries
|
||
import MyService.Handler as H -- handler der H.myApiEndpointV1Post implementiert
|
||
import MyService.Types -- weitere Type (s. nächste box)
|
||
import MyServiceGen.API as MS -- aus der generierten library
|
||
|
||
|
||
myServicemain :: IO ()
|
||
myServicemain = do
|
||
-- .env-Datei ins Prozess-Environment laden, falls noch nicht von außen gesetzt
|
||
void $ loadFile $ Dotenv.Config [".env"] [] False
|
||
-- Config holen (defaults + overrides aus dem Environment)
|
||
sc@ServerConfig{..} <- decodeWithDefaults defConfig
|
||
-- Backend-Setup
|
||
-- legt sowas wie Proxy-Server fest und wo man wie dran kommt. Benötigt für das Sprechen mit anderen Microservices
|
||
let defaultHTTPSSettings = UseDefaultHTTPSSettings.tlsManagerSettings { managerResponseTimeout = responseTimeoutMicro $ 1000 * 1000 * myserviceMaxTimeout }
|
||
createBackend url proxy = do
|
||
manager <- newManager . managerSetProxy proxy
|
||
$ defaultHTTPSSettings
|
||
url' <- parseBaseUrl url
|
||
return (mkClientEnv manager url')
|
||
internalProxy = case myserviceInternalProxyUrl of
|
||
"" -> noProxy
|
||
url -> useProxy $ HTTP.Proxy (fromString url) myserviceInternalProxyPort
|
||
-- externalProxy = case myserviceExternalProxyUrl of
|
||
-- "" -> noProxy
|
||
-- url -> useProxy $ HTTP.Proxy (fromString url) myserviceExternalProxyPort
|
||
|
||
-- Definieren & Erzeugen der Funktionen um die anderen Services anzusprechen.
|
||
calls <- (,)
|
||
<$> createBackend myserviceAUri internalProxy
|
||
<*> createBackend myserviceBUri internalProxy
|
||
|
||
-- Logging-Setup
|
||
hSetBuffering stdout LineBuffering
|
||
hSetBuffering stderr LineBuffering
|
||
|
||
|
||
-- Infos holen, brauchen wir später
|
||
myName <- getHostName
|
||
today <- formatTime defaultTimeLocale "%F" . utctDay <$> getCurrentTime
|
||
|
||
|
||
-- activeMQ-Transaktional-Queue zum schreiben nachher vorbereiten
|
||
amqPost <- newTQueueIO
|
||
|
||
|
||
-- bracket a b c == erst a machen, ergebnis an c als variablen übergeben. Schmeisst c ne exception/wird gekillt/..., werden die variablen an b übergeben.
|
||
bracket
|
||
-- logfiles öffnen
|
||
(LogFiles <$> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".info") AppendMode
|
||
<*> openFile (if myserviceDebug then "/logs/myservice-"<>myName<>"-"<>today<>".debug" else "/dev/null") AppendMode
|
||
<*> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".error") AppendMode
|
||
<*> openFile ("/logs/myservice-"<>myName<>"-"<>today<>".timings") AppendMode
|
||
)
|
||
-- und bei exception/beendigung schlißen.h
|
||
(\(LogFiles a b c d) -> mapM_ hClose [a,b,c,d])
|
||
$ \logfiles -> do
|
||
|
||
|
||
-- logschreibe-funktionen aliasen; log ist hier abstrakt, iolog spezialisiert auf io.
|
||
let log = printLogFiles logfiles :: MonadIO m => [LogItem] -> m ()
|
||
iolog = printLogFilesIO logfiles :: [LogItem] -> IO ()
|
||
|
||
|
||
-- H.myApiEndpointV1Post ist ein Handler (alle Handler werden mit alias H importiert) und in einer eigenen Datei
|
||
-- Per Default bekommen Handler sowas wie die server-config, die Funktionen um mit anderen Services zu reden, die AMQ-Queue um ins Kibana zu loggen und eine Datei-Logging-Funktion
|
||
-- Man kann aber noch viel mehr machen - z.b. gecachte Daten übergeben, eine Talk-Instanz, etc. pp.
|
||
server = MyServiceBackend{ myApiEndpointV1Post = H.myApiEndpointV1Post sc calls amqPost log
|
||
}
|
||
config = MS.Config $ "http://" ++ myserviceHost ++ ":" ++ show myservicePort ++ "/"
|
||
iolog . pure . Info $ "Using Server configuration:"
|
||
iolog . pure . Info $ pretty sc { myserviceActivemqPassword = "******" -- Do NOT log the password ;)
|
||
, myserviceMongoPassword = "******"
|
||
}
|
||
-- alle Services starten (Hintergrund-Aktionen wie z.b. einen MongoDB-Dumper, einen Talk-Server oder wie hier die ActiveMQ
|
||
void $ forkIO $ keepActiveMQConnected sc iolog amqPost
|
||
-- logging-Framework erzeugen
|
||
loggingMW <- loggingMiddleware
|
||
-- server starten
|
||
if myserviceDebug
|
||
then runMyServiceMiddlewareServer config (cors (\_ -> Just (simpleCorsResourcePolicy {corsRequestHeaders = ["Content-Type"]})) . loggingMW . logStdout) server
|
||
else runMyServiceMiddlewareServer config (cors (\_ -> Just (simpleCorsResourcePolicy {corsRequestHeaders = ["Content-Type"]}))) server
|
||
|
||
|
||
-- Sollte bald in die Library hs-stomp ausgelagert werden
|
||
-- ist ein Beispiel für einen ActiveMQ-Dumper
|
||
keepActiveMQConnected :: ServerConfig -> ([LogItem] -> IO ()) -> TQueue BS.ByteString -> IO ()
|
||
keepActiveMQConnected sc@ServerConfig{..} printLog var = do
|
||
res <- handle (\(e :: SomeException) -> do
|
||
printLog . pure . Error $ "Exception in AMQ-Thread: "<>show e
|
||
return $ Right ()
|
||
) $ AMQ.try $ do -- catches all AMQ-Exception that we can handle. All others bubble up.
|
||
printLog . pure . Info $ "AMQ: connecting..."
|
||
withConnection myserviceActivemqHost myserviceActivemqPort [ OAuth myserviceActivemqUsername myserviceActivemqPassword
|
||
, OTmo (30*1000) {- 30 sec timeout -}
|
||
]
|
||
[] $ \c -> do
|
||
let oconv = return
|
||
printLog . pure . Info $ "AMQ: connected"
|
||
withWriter c "Chaos-Logger for Kibana" "chaos.logs" [] [] oconv $ \writer -> do
|
||
printLog . pure . Info $ "AMQ: queue created"
|
||
let postfun = writeQ writer (Type (Application "json") []) []
|
||
void $ race
|
||
(forever $ atomically (readTQueue var) >>= postfun)
|
||
(threadDelay (600*1000*1000)) -- wait 10 Minutes
|
||
-- close writer
|
||
-- close connection
|
||
-- get outside of all try/handle/...-constructions befor recursing.
|
||
case res of
|
||
Left ex -> do
|
||
printLog . pure . Error $ "AMQ: "<>show ex
|
||
keepActiveMQConnected sc printLog var
|
||
Right _ -> keepActiveMQConnected sc printLog var
|
||
|
||
|
||
-- Beispiel für eine Custom-Logging-Middleware.
|
||
-- Hier werden z.B. alle 4xx-Status-Codes inkl. Payload ins stdout-Log geschrieben.
|
||
-- Nützlich, wenn die Kollegen ihre Requests nicht ordentlich schreiben können und der Server das Format zurecht mit einem BadRequest ablehnt ;)
|
||
loggingMiddleware :: IO Middleware
|
||
loggingMiddleware = liftIO $ mkRequestLogger $ def { outputFormat = CustomOutputFormatWithDetails out }
|
||
where
|
||
out :: ZonedDate -> WAI.Request -> Status -> Maybe Integer -> NominalDiffTime -> [BS.ByteString] -> Builder -> LogStr
|
||
out _ r status _ _ payload _
|
||
| statusCode status < 300 = ""
|
||
| statusCode status > 399 && statusCode status < 500 = "Error code "<>toLogStr (statusCode status) <>" sent. Request-Payload was: "<> mconcat (toLogStr <$> payload) <> "\n"
|
||
| otherwise = toLogStr (show r) <> "\n"
|
||
</code></pre></div>
|
||
|
||
</div>
|
||
</details>
|
||
</section>
|
||
<h4 id='weitere-instanzen-und-definitionen-die-der-generator-noch-nicht-macht' class='mt-6 mb-2 text-2xl font-bold text-gray-700'>Weitere Instanzen und Definitionen, die der Generator (noch) nicht macht</h4>
|
||
<p class='mb-3'>
|
||
In der <code class='py-0.5 px-0.5 bg-gray-100'>Myservice.Types</code> werden ein paar hilfreiche Typen und Typ-Instanzen definiert. Im Folgenden geht es dabei um Dinge für:
|
||
</p>
|
||
|
||
<ul class='my-3 ml-6 space-y-1 list-disc'>
|
||
|
||
<li>
|
||
<code class='py-0.5 px-0.5 bg-gray-100'>Envy</code>
|
||
<ul class='my-3 ml-6 space-y-1 list-disc'>
|
||
|
||
<li>
|
||
Laden von <code class='py-0.5 px-0.5 bg-gray-100'>$ENV_VAR</code> in Datentypen
|
||
</li>
|
||
|
||
<li>
|
||
Definitionen für Default-Settings
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
</li>
|
||
|
||
<li>
|
||
<code class='py-0.5 px-0.5 bg-gray-100'>ServerConfig</code>
|
||
<ul class='my-3 ml-6 space-y-1 list-disc'>
|
||
|
||
<li>
|
||
Definition der Server-Konfiguration & Benennung der Environment-Variablen
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
</li>
|
||
|
||
<li>
|
||
<code class='py-0.5 px-0.5 bg-gray-100'>ExtraTypes</code>
|
||
<ul class='my-3 ml-6 space-y-1 list-disc'>
|
||
|
||
<li>
|
||
ggf. Paketweite extra-Typen, die der Generator nicht macht, weil sie nicht aus der API kommen (z.B. cache)
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
</li>
|
||
|
||
<li>
|
||
<code class='py-0.5 px-0.5 bg-gray-100'>Out</code>/<code class='py-0.5 px-0.5 bg-gray-100'>BSON</code>-Instanzen
|
||
<ul class='my-3 ml-6 space-y-1 list-disc'>
|
||
|
||
<li>
|
||
Der API-Generator generiert nur wenige Instanzen automatisch (z.B. <code class='py-0.5 px-0.5 bg-gray-100'>aeson</code>), daher werden hier die fehlenden definiert.
|
||
</li>
|
||
|
||
<li>
|
||
<code class='py-0.5 px-0.5 bg-gray-100'>BSON</code>: Kommunikation mit <code class='py-0.5 px-0.5 bg-gray-100'>MongoDB</code>
|
||
</li>
|
||
|
||
<li>
|
||
<code class='py-0.5 px-0.5 bg-gray-100'>Out</code>: pretty-printing im Log
|
||
<ul class='my-3 ml-6 space-y-1 list-disc'>
|
||
|
||
<li>
|
||
Nur nötig, wenn man pretty-printing via <code class='py-0.5 px-0.5 bg-gray-100'>Out</code> statt über Generics wie z.b. <code class='py-0.5 px-0.5 bg-gray-100'>pretty-generic</code> oder die automatische Show-Instanz via <code class='py-0.5 px-0.5 bg-gray-100'>prerryShow</code> macht.
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
<section title='Embedded note' class='p-4 mx-2 mb-2 bg-white border-2 rounded-lg shadow-inner'>
|
||
<details>
|
||
<summary class='flex items-center justify-center text-2xl italic bg-purple-50 rounded py-1 px-2 mb-3'>
|
||
<header style='display:list-item'>
|
||
<a href='Coding/Haskell/Webapp-Example/MyService_Types.hs'>
|
||
Webapp-Example: MyService/Types.hs
|
||
</a>
|
||
</header>
|
||
</summary>
|
||
<div>
|
||
|
||
<p class='mb-3'>
|
||
Anleitung siehe <a href='Coding/Haskell/Webapp-Example' class='text-purple-600 mavenLinkBold hover:underline' data-wikilink-type='WikiLinkTag'>Webapp-Development in Haskell</a>.
|
||
</p>
|
||
<div class='py-0.5 mb-3 text-sm'><pre><code class='haskell language-haskell'>{-# OPTIONS_GHC -Wno-orphans #-}
|
||
{-# OPTIONS_GHC -Wno-name-shadowing #-}
|
||
{-# LANGUAGE DeriveAnyClass #-}
|
||
{-# LANGUAGE DeriveFunctor #-}
|
||
{-# LANGUAGE DeriveGeneric #-}
|
||
{-# LANGUAGE DerivingVia #-}
|
||
{-# LANGUAGE DuplicateRecordFields #-}
|
||
{-# LANGUAGE FlexibleContexts #-}
|
||
{-# LANGUAGE FlexibleInstances #-}
|
||
{-# LANGUAGE GADTs #-}
|
||
{-# LANGUAGE LambdaCase #-}
|
||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||
{-# LANGUAGE OverloadedStrings #-}
|
||
{-# LANGUAGE RankNTypes #-}
|
||
{-# LANGUAGE RecordWildCards #-}
|
||
module MyService.Types where
|
||
|
||
import Data.Aeson (FromJSON, ToJSON)
|
||
import Data.Text
|
||
import Data.Time.Clock
|
||
import GHC.Generics
|
||
import System.Envy
|
||
import Text.PrettyPrint (text)
|
||
import Text.PrettyPrint.GenericPretty
|
||
|
||
-- Out hat hierfür keine Instanzen, daher kurz eine einfach Definition.
|
||
instance Out Text where
|
||
doc = text . unpack
|
||
docPrec i a = text $ showsPrec i a ""
|
||
|
||
instance Out UTCTime where
|
||
doc = text . show
|
||
docPrec i a = text $ showsPrec i a ""
|
||
|
||
-- Der ServerConfig-Typ. Wird mit den defaults unten initialisiert, dann mit den Variablen aus der .env-Datei überschrieben und zum Schluss können Serveradmins diese via $MYSERVICE_FOO nochmal überschreiben.
|
||
data ServerConfig = ServerConfig
|
||
{ myserviceHost :: String -- ^ Environment: $MYSERVICE_HOST
|
||
, myservicePort :: Int -- ^ Environment: $MYSERVICE_PORT
|
||
, myserviceMaxTimeout :: Int -- ^ Environment: $MYSERVICE_MAX_TIMEOUT
|
||
, myserviceInternalProxyUrl :: String -- ^ Environment: $MYSERVICE_INTERNAL_PROXY_URL
|
||
, myserviceInternalProxyPort :: Int -- ^ Environment: $MYSERVICE_INTERNAL_PROXY_PORT
|
||
, myserviceExternalProxyUrl :: String -- ^ Environment: $MYSERVICE_EXTERNAL_PROXY_URL
|
||
, myserviceExternalProxyPort :: Int -- ^ Environment: $MYSERVICE_EXTERNAL_PROXY_PORT
|
||
, myserviceActivemqHost :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_HOST
|
||
, myserviceActivemqPort :: Int -- ^ Environment: $MYSERVICE_ACTIVEMQ_PORT
|
||
, myserviceActivemqUsername :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_USERNAME
|
||
, myserviceActivemqPassword :: String -- ^ Environment: $MYSERVICE_ACTIVEMQ_PASSWORD
|
||
, myserviceMongoUsername :: String -- ^ Environment: $MYSERVICE_MONGO_USERNAME
|
||
, myserviceMongoPassword :: String -- ^ Environment: $MYSERVICE_MONGO_PASSWORD
|
||
, myserviceDebug :: Bool -- ^ Environment: $MYSERVICE_DEBUG
|
||
} deriving (Show, Eq, Generic)
|
||
|
||
-- Default-Konfigurations-Instanz für diesen Service.
|
||
instance DefConfig ServerConfig where
|
||
defConfig = ServerConfig "0.0.0.0" 8080 20
|
||
""
|
||
""
|
||
""
|
||
0
|
||
""
|
||
0
|
||
""
|
||
0
|
||
""
|
||
""
|
||
""
|
||
""
|
||
False
|
||
|
||
-- Kann auch aus dem ENV gefüllt werden
|
||
instance FromEnv ServerConfig
|
||
-- Und hübsch ausgegeben werden.
|
||
instance Out ServerConfig
|
||
|
||
|
||
instance Out Response
|
||
instance FromBSON Repsonse -- FromBSON-Instanz geht immer davon aus, dass alle keys da sind (ggf. mit null bei Nothing).</code></pre></div>
|
||
|
||
</div>
|
||
</details>
|
||
</section>
|
||
<h4 id='was-noch-zu-tun-ist' class='mt-6 mb-2 text-2xl font-bold text-gray-700'>Was noch zu tun ist</h4>
|
||
<p class='mb-3'>
|
||
Den Service implementieren. Einfach ein neues Modul aufmachen (z.B. <code class='py-0.5 px-0.5 bg-gray-100'>MyService.Handler</code> oder <code class='py-0.5 px-0.5 bg-gray-100'>MyService.DieserEndpunktbereich</code>/<code class='py-0.5 px-0.5 bg-gray-100'>MyService.JenerEndpunktbereich</code>) und dort die Funktion implementieren, die man in der <code class='py-0.5 px-0.5 bg-gray-100'>Main.hs</code> benutzt hat. In dem Handler habt ihr dann keinen Stress mehr mit Validierung, networking, logging, etc. pp. weil alles in der Main abgehandelt wurde und ihr nur noch den “Happy-Case” implementieren müsst. Beispiel für unseren Handler oben:
|
||
</p>
|
||
<div class='py-0.5 mb-3 text-sm'><pre><code class='haskell language-haskell'>myApiEndpointV1Post :: MonadIO m => ServerConfig -> (ClientEnv,ClientEnv) -> TQueue BS.ByteString -> ([LogItem] -> IO ()) -> Request -> m Response
|
||
myApiEndpointV1Post sc calls amqPost log req = do
|
||
liftIO . log $ [Info $ "recieved "<>pretty req] -- input-logging
|
||
liftIO . atomically . writeTQueue . LBS.toStrict $ "{\"hey Kibana, i recieved:\"" <> A.encode (pretty req) <> "}" -- log in activeMQ/Kibana
|
||
|
||
|
||
--- .... gaaaanz viel komplizierter code um die Response zu erhalten ;)
|
||
let ret = Response 1337 Nothing -- dummy-response ;)
|
||
-- gegeben wir haben eine gültige mongodb-pipe;
|
||
-- mehr logik will ich in die Beispiele nicht packen.
|
||
-- Man kann die z.b. als weiteren Wert in einer TMVar (damit man sie ändern & updaten kann) an die Funktion übergeben.
|
||
liftIO . access pipe master "DatabaseName" $ do
|
||
ifM (auth (myServiceMongoUsername sc) (myServiceMongoPassword sc)) (return ()) (liftIO . printLog . pure . Error $ "MongoDB: Login failed.")
|
||
save "DatabaseCollection" ["_id" =: 1337, "entry" =: ret] -- selbe id wie oben ;)
|
||
return ret</code></pre></div>
|
||
<p class='mb-3'>
|
||
Diese dummy-Antwort führt auf, wie gut man die ganzen Sachen mischen kann.
|
||
</p>
|
||
|
||
<ul class='my-3 ml-6 space-y-1 list-disc'>
|
||
|
||
<li>
|
||
Logging in die Dateien/<code class='py-0.5 px-0.5 bg-gray-100'>stdout</code> - je nach Konfiguration
|
||
</li>
|
||
|
||
<li>
|
||
Logging von Statistiken in Kibana
|
||
</li>
|
||
|
||
<li>
|
||
Speichern der Antwort in der MongoDB
|
||
</li>
|
||
|
||
<li>
|
||
Generieren einer Serverantwort und ausliefern dieser über die Schnittstelle
|
||
</li>
|
||
|
||
</ul>
|
||
<h4 id='tipps--tricks' class='mt-6 mb-2 text-2xl font-bold text-gray-700'>Tipps & Tricks</h4><h5 id='dateien-die-statisch-ausgeliefert-werden-sollen' class='mt-6 mb-2 text-xl font-bold text-gray-700'>Dateien, die statisch ausgeliefert werden sollen</h5>
|
||
<p class='mb-3'>
|
||
Hierzu erstellt man ein Verzeichnis <code class='py-0.5 px-0.5 bg-gray-100'>static/</code> (Konvention; ist im generator so generiert, dass das ausgeliefert wird). Packt man hier z.b. eine <code class='py-0.5 px-0.5 bg-gray-100'>index.html</code> rein, erscheint die, wenn man den Service ansurft.
|
||
</p>
|
||
<h5 id='wie-bekomme-ich-diese-fancy-preview-hin' class='mt-6 mb-2 text-xl font-bold text-gray-700'>Wie bekomme ich diese fancy Preview hin?</h5>
|
||
<p class='mb-3'>
|
||
Der Editor, der ganz am Anfang zum Einsatz gekommen ist, braucht nur die <code class='py-0.5 px-0.5 bg-gray-100'>api-doc.yml</code> um diese Ansicht zu erzeugen. Daher empfiehlt sich hier ein angepasster Fork davon indem die Pfade in der index.html korrigiert sind. Am einfachsten (und von den meisten services so benutzt): In meiner Implementation liegt dann nach dem starten auf http://localhost:PORT/ui/ und kann direkt dort getestet werden.
|
||
</p>
|
||
<h5 id='wie-sorge-ich-für-bessere-warnungen-damit-der-compiler-meine-bugs-fängt' class='mt-6 mb-2 text-xl font-bold text-gray-700'>Wie sorge ich für bessere Warnungen, damit der Compiler meine Bugs fängt?</h5><div class='py-0.5 mb-3 text-sm'><pre><code class='bash language-bash'>stack build --file-watch --ghc-options '-freverse-errors -W -Wall -Wcompat' --interleaved-output</code></pre></div>
|
||
<p class='mb-3'>
|
||
Was tut das?
|
||
</p>
|
||
|
||
<ul class='my-3 ml-6 space-y-1 list-disc'>
|
||
|
||
<li>
|
||
<code class='py-0.5 px-0.5 bg-gray-100'>--file-watch</code>: automatisches (minimales) kompilieren bei dateiänderungen
|
||
</li>
|
||
|
||
<li>
|
||
<code class='py-0.5 px-0.5 bg-gray-100'>--ghc-options</code>
|
||
<ul class='my-3 ml-6 space-y-1 list-disc'>
|
||
|
||
<li>
|
||
<code class='py-0.5 px-0.5 bg-gray-100'>-freverse-errors</code>: Fehlermeldungen in umgekehrter Reihenfolge (Erster Fehler ganz unten; wenig scrollen )
|
||
</li>
|
||
|
||
<li>
|
||
<code class='py-0.5 px-0.5 bg-gray-100'>-W</code>: Warnungen an
|
||
</li>
|
||
|
||
<li>
|
||
<code class='py-0.5 px-0.5 bg-gray-100'>-Wall</code>: Alle sinnvollen Warnungen an (im gegensatz zu <code class='py-0.5 px-0.5 bg-gray-100'>-Weverything</code>, was WIRKLICH alles ist )
|
||
</li>
|
||
|
||
<li>
|
||
<code class='py-0.5 px-0.5 bg-gray-100'>-Wcompat</code>: Warnungen für Sachen, die in der nächsten Compilerversion kaputt brechen werden & vermieden werden sollten
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
</li>
|
||
|
||
<li>
|
||
<code class='py-0.5 px-0.5 bg-gray-100'>--interleaved-output</code>: stack-log direkt ausgeben & nicht in Dateien schreiben und die dann am ende zusammen cat'en.
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
<p class='mb-3'>
|
||
Um pro Datei Warnungen auszuschalten (z.B. weil man ganz sicher weiss, was man tut -.-): <code class='py-0.5 px-0.5 bg-gray-100'>{-# OPTIONS_GHC -Wno-whatsoever #-}</code> als pragma in die Datei.
|
||
</p>
|
||
|
||
<p class='mb-3'>
|
||
<strong>Idealerweise sollte das Projekt keine Warnungen erzeugen.</strong>
|
||
</p>
|
||
<h3 id='deployment' class='mt-6 mb-2 text-3xl font-bold text-gray-700'>Deployment</h3>
|
||
<p class='mb-3'>
|
||
Als Beispiel sei hier ein einfaches Docker-Build mit Jenkins-CI gezeigt, weil ich das aus Gründen rumliegen hatte. Kann man analog in fast alle anderen CI übersetzen.
|
||
</p>
|
||
<h4 id='docker' class='mt-6 mb-2 text-2xl font-bold text-gray-700'>Docker</h4>
|
||
<p class='mb-3'>
|
||
Die angehängten Scripte gehen von einer Standard-Einrichtung aus (statische Sachen in static, 2-3 händische Anpassungen auf das eigene Projekt nach auspacken). Nachher liegt dann auch unter static/version die gebaute Versionsnummer & kann abgerufen werden. In der <code class='py-0.5 px-0.5 bg-gray-100'>Dockerfile.release</code> und der <code class='py-0.5 px-0.5 bg-gray-100'>Jenkinsfile</code> müssen noch Anpassungen gemacht werden. Konkret:
|
||
</p>
|
||
|
||
<ul class='my-3 ml-6 space-y-1 list-disc'>
|
||
|
||
<li>
|
||
in der <code class='py-0.5 px-0.5 bg-gray-100'>Dockerfile.release</code>: alle <code class='py-0.5 px-0.5 bg-gray-100'><<<HIER>>></code>-Stellen sinnvoll befüllen
|
||
</li>
|
||
|
||
<li>
|
||
in der <code class='py-0.5 px-0.5 bg-gray-100'>Jenkinsfile</code> die defs für “servicename” und “servicebinary” ausfüllen. Binary ist das, was bei stack exec aufgerufen wird; name ist der Image-Name für das docker-repository.
|
||
</li>
|
||
|
||
</ul>
|
||
<h4 id='jenkins' class='mt-6 mb-2 text-2xl font-bold text-gray-700'>Jenkins</h4>
|
||
<p class='mb-3'>
|
||
Änderungen die dann noch gemacht werden müssen:
|
||
</p>
|
||
|
||
<ul class='my-3 ml-6 space-y-1 list-disc'>
|
||
|
||
<li>
|
||
git-repository URL anpassen
|
||
</li>
|
||
|
||
<li>
|
||
Environment-Vars anpassen ($BRANCH = test & live haben keine zusatzdinger im docker-image-repository; ansonsten hat das image $BRANCH im Namen)
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
<p class='mb-3'>
|
||
Wenn das fertig gebaut ist, liegt im test/live-repository ein docker-image namens <code class='py-0.5 px-0.5 bg-gray-100'>servicename:version</code>.
|
||
</p>
|
||
<h3 id='omg-ich-muss-meine-api-ändern-was-mache-ich-nun' class='mt-6 mb-2 text-3xl font-bold text-gray-700'>OMG! Ich muss meine API ändern. Was mache ich nun?</h3>
|
||
<ul class='my-3 ml-6 space-y-1 list-decimal list-inside'>
|
||
|
||
<li>
|
||
api-doc.yml bearbeiten, wie gewünscht
|
||
</li>
|
||
|
||
<li>
|
||
mittels generator die Api & submodule neu generieren
|
||
</li>
|
||
|
||
<li>
|
||
ggf. custom Änderungen übernehmen (:Gitdiffsplit hilft)
|
||
</li>
|
||
|
||
<li>
|
||
Alle Compilerfehler + Warnungen in der eigentlichen Applikation fixen
|
||
</li>
|
||
|
||
<li>
|
||
If it comipilez, ship it! (Besser nicht <span class='emoji' data-emoji='grin' style='font-family: emoji'>😁</span>)
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
|
||
<!-- div class="flex items-center justify-center mt-2">
|
||
<ema:metadata>
|
||
<with var="template">
|
||
<a class="text-gray-300 hover:text-${theme}-600 text-sm" title="Edit this page on GitHub"
|
||
href="${value:editBaseUrl}/${ema:note:source-path}">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||
</svg>
|
||
</a>
|
||
</with>
|
||
</ema:metadata>
|
||
</div -->
|
||
</article>
|
||
<div class='flex flex-col lg:flex-row lg:space-x-2'>
|
||
|
||
|
||
<div class='flex-1 p-4 mt-8 bg-gray-100 rounded'>
|
||
<header class='mb-2 text-xl font-semibold text-gray-500'>Links to this page</header>
|
||
<ul class='space-y-1'>
|
||
|
||
<li>
|
||
<a class='text-purple-600 mavenLinkBold hover:bg-purple-50' href='Coding/Haskell/Webapp-Example/MyService_Types.hs'>
|
||
Webapp-Example: MyService/Types.hs
|
||
</a>
|
||
|
||
<div class='mb-4 overflow-auto text-sm text-gray-500'>
|
||
|
||
<div class='pl-2 mt-2 border-l-2 border-purple-200 hover:border-purple-500'>
|
||
<div><p>Anleitung siehe <a href='Coding/Haskell/Webapp-Example' class='text-gray-600 font-bold hover:bg-gray-50' data-wikilink-type='WikiLinkTag'>Webapp-Development in Haskell</a>.</p></div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</li>
|
||
|
||
<li>
|
||
<a class='text-purple-600 mavenLinkBold hover:bg-purple-50' href='Coding/Haskell/Webapp-Example/Main.hs'>
|
||
Webapp-Example: Main.hs
|
||
</a>
|
||
|
||
<div class='mb-4 overflow-auto text-sm text-gray-500'>
|
||
|
||
<div class='pl-2 mt-2 border-l-2 border-purple-200 hover:border-purple-500'>
|
||
<div><p>Wie man das verwendet, siehe <a href='Coding/Haskell/Webapp-Example' class='text-gray-600 font-bold hover:bg-gray-50' data-wikilink-type='WikiLinkTag'>Webapp-Development in Haskell</a>.</p></div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</li>
|
||
|
||
<li>
|
||
<a class='text-purple-600 mavenLinkBold hover:bg-purple-50' href='Coding/OpenAPI'>
|
||
Openapi-generator
|
||
</a>
|
||
|
||
<div class='mb-4 overflow-auto text-sm text-gray-500'>
|
||
|
||
<div class='pl-2 mt-2 border-l-2 border-purple-200 hover:border-purple-500'>
|
||
<div><p>Wie im <a href='Coding/Haskell/Webapp-Example' class='text-gray-600 font-bold hover:bg-gray-50' data-wikilink-type='WikiLinkNormal'>Webapp-Development in Haskell</a> kurz angerissen wird in Haskell nicht zwischen Server und Client unterschieden. Daher können hier sehr viele Optimierungen bei Änderungen passieren, die in anderen Sprachen nicht so einfach möglich sind.</p></div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<section class='flex flex-wrap items-end justify-center my-4 space-x-2 space-y-2 font-mono text-sm'>
|
||
|
||
</section>
|
||
|
||
<!-- What goes in this file will at the very end of the main div -->
|
||
</main>
|
||
</div>
|
||
</div>
|
||
<footer class='flex items-center justify-center mt-2 mb-8 space-x-4 text-center text-gray-800'>
|
||
|
||
<div>
|
||
<a href='' title='Go to Home page'>
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-6 h-6 hover:text-purple-700' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6'></path>
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
<div>
|
||
<a href='-/all' title='View Index'>
|
||
<svg class='w-6 h-6 hover:text-purple-700' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4'>
|
||
</path>
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
<div>
|
||
<a href='https://emanote.srid.ca' target='_blank' title='Generated by Emanote 1.0.3.11'>
|
||
<img class='w-6 h-6 hover:text-purple-700' src='_emanote-static/emanote-logo.svg' />
|
||
</a>
|
||
</div>
|
||
<div>
|
||
<a href='-/tags' title='View tags'>
|
||
<svg class='w-6 h-6 hover:text-purple-700' fill='none' stroke='currentColor' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z'>
|
||
</path>
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
<div>
|
||
<a href='-/tasks' title='View tasks'>
|
||
<svg xmlns='http://www.w3.org/2000/svg' class='w-6 h-6 hover:text-purple-700' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
|
||
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z'></path>
|
||
</svg>
|
||
</a>
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
|
||
<div id='stork-search-container' class='hidden fixed w-screen h-screen inset-0 backdrop-filter backdrop-blur-sm'>
|
||
<div class='fixed w-screen h-screen inset-0' onclick='window.emanote.stork.toggleSearch()'></div>
|
||
|
||
<div class='container mx-auto p-10 mt-10'>
|
||
<div class='stork-wrapper-flat container mx-auto'>
|
||
<input id='stork-search-input' data-stork='emanote-search' class='stork-input' placeholder='Search (Ctrl+K) ...' />
|
||
<div data-stork='emanote-search-output' class='stork-output'></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
</body>
|
||
|
||
</html>
|