commit 440559dc57779c53b24c8de3f51b11399c70dc28 Author: github-classroom[bot] <66690702+github-classroom[bot]@users.noreply.github.com> Date: Tue Feb 21 19:09:29 2023 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d346e49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,111 @@ +# Generated files +upload/ +build.json + +# Finder +.DS_Store + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/README.md b/README.md new file mode 100644 index 0000000..6166d05 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# MSDE Software Architecture 2022 + +Modeling Assignment Repository + +## Getting started + +``` +brew install yarn plantuml +yarn install +yarn global add onchange +``` + +For installation of brew take a look at: + +* https://brew.sh/ + +Make sure that your `java` version is compatible with `plantuml` + +## Building the document + +``` +yarn build +``` + +The document [Markdown](https://www.markdownguide.org/cheat-sheetplan) and [PlantUML](https://plantuml.com/) source is found in the `src/sa/model/` folder. + +Write your assignments extending the template `index.md` as you solve each exercise. + +Enter your project title and your name in the header. + +``` +--- +title: +Model of My Project +--- +author: +Enter Your Full Name +--- +``` + +The HTML output and the generated SVG diagrams are stored in the `upload` folder. + +## Clean the Output +``` +yarn clean +``` + +## Continuous Build +``` +yarn watch +``` + +This requires `yarn global add onchange` to work. It will automatically rebuild the documentation when the source files are modified. Stop it using `^C`. + + +## Submitting Your Work + +After adding (with `git add`) whatever file you have modified (please **do not** include the generated files inside the `upload` folder), use the following commit message to indicate your work is ready to be checked: + +``` +git commit -m "exercise N complete, please check" +git push +``` + +where N is the assignment number (0-15). + +We will use github issues to provide feedback about your model. + +Also, be ready to present and discuss your work during lectures. + + + diff --git a/build.js b/build.js new file mode 100644 index 0000000..75b8160 --- /dev/null +++ b/build.js @@ -0,0 +1,593 @@ +const { PerformanceObserver, performance } = require('perf_hooks'); +let t_start = performance.now(); + +process.on('exit', function () { + let t_end = performance.now(); + console.log("Done in " + (t_end - t_start) + "ms"); +}); + +const fs = require('fs-extra'); +const glob = require('glob-fs')({ gitignore: true }); +const marked = require('marked'); +const marked_toc = require('marked'); +const plantuml = require('plantuml'); +const _ = require('lodash'); +const slugify = require('uslug'); + + + + +const child_process = require('child_process'); + + +function run(cmd, cwd, done) { + + console.log(cmd); + + const exec = child_process.exec; + exec(cmd, { cwd }, (err, stdout, stderr) => { + if (err) { + //some err occurred + console.error(err); + } else { + // the *entire* stdout and stderr (buffered) + console.log(`stdout: ${stdout}`); + console.log(`stderr: ${stderr}`); + if (typeof done == "function") { + done(err, stdout, stderr); + } else { } + } + }); + +} + + +const build_json = fs.readJSONSync('build.json', { throws: false }) || {}; + +build_json.doc_build = (build_json.doc_build || 0) + 1; + +fs.outputJsonSync('build.json', build_json, { spaces: 2 }); + + +String.prototype.hashCode = function () { + var hash = 0, i, chr; + if (this.length === 0) return hash; + for (i = 0; i < this.length; i++) { + chr = this.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; // Convert to 32bit integer + } + return hash; +}; + +const cache_json = fs.readJSONSync('./upload/puml_cache.json', { throws: false }) || {}; + + +function cache_lookup(text, miss) { + let hash = new String(text).hashCode(); + + if (cache_json[hash]) { //hit + return new Promise((resolve, reject) => resolve(cache_json[hash])); + } else { + return miss(text).then((output) => { + cache_json[hash] = output; + fs.outputJson('./upload/puml_cache.json', cache_json, { spaces: 2 }); + return output; + }) + } +} + + +let puml_count = 0; +let epuml_count = 0; +let fml_count = 0; + + +function toc(str) { + + // var defaultTemplate = '<%= depth %><%= bullet %>[<%= heading %>](#<%= url %>)\n'; + var defaultTemplate = '<%- heading %>\n'; + + const utils = {}; + + utils.arrayify = function (arr) { + return !Array.isArray(arr) ? [arr] : arr; + }; + + utils.escapeRegex = function (re) { + return re.replace(/(\[|\]|\(|\)|\/|\.|\^|\$|\*|\+|\?)/g, '\\$1'); + }; + + utils.isDest = function (dest) { + return !dest || dest === 'undefined' || typeof dest === 'object'; + }; + + utils.isMatch = function (keys, str) { + keys = utils.arrayify(keys); + keys = (keys.length > 0) ? keys.join('|') : '.*'; + + // Escape certain characters, like '[', '(' + var k = utils.escapeRegex(String(keys)); + + // Build up the regex to use for replacement patterns + var re = new RegExp('(?:' + k + ')', 'g'); + if (String(str).match(re)) { + return true; + } else { + return false; + } + }; + + utils.sanitize = function (src) { + src = src.replace(/(\s*\[!|(?:\[.+ →\]\()).+/g, ''); + src = src.replace(/\s*\*\s*\[\].+/g, ''); + return src; + }; + + utils.slugify = function (str) { + str = str.replace(/\/\//g, '-'); + str = str.replace(/\//g, '-'); + str = str.replace(/\./g, '-'); + str = _.str.slugify(str); + str = str.replace(/^-/, ''); + str = str.replace(/-$/, ''); + return str; + }; + + var omit = ['grunt', 'helper', 'handlebars-helper', 'mixin', 'filter', 'assemble-contrib', 'assemble']; + + utils.strip = function (name, options) { + var opts = _.extend({}, options); + if(opts.omit === false) {omit = [];} + var exclusions = _.union(omit, utils.arrayify(opts.strip || [])); + var re = new RegExp('^(?:' + exclusions.join('|') + ')[-_]?', 'g'); + return name.replace(re, ''); + }; + + var opts = { + firsth1: false, + blacklist: true, + omit: [], + maxDepth: 3, + slugifyOptions: { allowedChars: '-' }, + slugify: function (text) { + return slugify(text, opts.slugifyOptions).replace("ex-","ex---"); + } + }; + + var toc = ''; + var tokens = marked.lexer(str); + var tocArray = []; + + // Remove the very first h1, true by default + if (opts.firsth1 === false) { + tokens.shift(); + } + + // Do any h1's still exist? + var h1 = _.some(tokens, { depth: 1 }); + + var task_count = 0; + + tokens.filter(function (token) { + // Filter out everything but headings + if (token.type !== 'heading' || token.type === 'code') { + return false; + } + + // Since we removed the first h1, we'll check to see if other h1's + // exist. If none exist, then we unindent the rest of the TOC + if (!h1) { + token.depth = token.depth - 1; + } + + // Store original text and create an id for linking + token.heading = opts.strip ? utils.strip(token.text, opts) : token.text; + + if (token.heading.indexOf("Ex -") >= 0) { + task_count++; + token.heading = token.heading.replace("Ex -", `${task_count}. `); + } + + // Create a "slugified" id for linking + token.id = opts.slugify(token.text); + + // Omit headings with these strings + var omissions = ['Table of Contents', 'TOC', 'TABLE OF CONTENTS']; + var omit = _.union([], opts.omit, omissions); + + if (utils.isMatch(omit, token.heading)) { + return; + } + + return true; + }).forEach(function (h) { + + if (h.depth > opts.maxDepth) { + return; + } + + var bullet = Array.isArray(opts.bullet) + ? opts.bullet[(h.depth - 1) % opts.bullet.length] + : opts.bullet; + + var data = _.extend({}, opts.data, { + d: h.depth, + depth: new Array((h.depth - 1) * 2 + 1).join(' '), + bullet: bullet ? bullet : '* ', + heading: h.heading, + url: h.id + }); + + tocArray.push(data); + toc += ejs.render(defaultTemplate,data); + //toc += _.template(opts.template || defaultTemplate, data); + }); + + return { + data: tocArray, + toc: opts.strip + ? utils.strip(toc, opts) + : toc + }; +} + + + +const renderer = new marked.Renderer(); + +renderer._paragraph = renderer.paragraph; + +renderer.paragraph = function (text) { + + //console.log(text); + + if (text.trim().startsWith("{")) { + + renderer.sec_depth = renderer.sec_depth || 0; + renderer.sec_depth++; + + let c = text.split("."); + c.shift(); + + if (c.length > 0) { + return `
`; + } else { + return "
" + } + } + + if (text.trim().startsWith("}")) { + + renderer.sec_depth = renderer.sec_depth || 0; + renderer.sec_depth--; + + return "
" + } + + if (text.trim().startsWith("Pass: ") && renderer.sec_depth > 0) { + return `
★☆☆: ${text.split("Pass: ")[1]}
`; + } + if (text.trim().startsWith("Good: ") && renderer.sec_depth > 0) { + return `
★★☆: ${text.split("Good: ")[1]}
`; + } + if (text.trim().startsWith("Exceed: ") && renderer.sec_depth > 0) { + return `
★★★: ${text.split("Exceed: ")[1]}
`; + } + if (text.trim().startsWith("Hint: ") && renderer.sec_depth > 0) { + return `
Hint: ${text.split("Hint: ")[1]}
`; + } + + return renderer._paragraph(text); +} + +renderer._code = renderer.code; + +renderer.code = function (code, infostring, escaped) { + //console.log(code, infostring, escaped); + if (infostring) { + if (infostring.startsWith("puml")) { + let i = epuml_count++; + cache_lookup(code, () => plantuml(code)).then(svg => { + fs.writeFile(renderer._out_folder + "puml_e" + i + ".svg", svg) + }) + return ``; + } + } + return renderer._code(code, infostring, escaped); + +}; + +renderer._image = renderer.image; + +renderer.image = function (href, title, text) { + + //console.log(href, title, text); + + if (href.endsWith(".puml")) { + + let i = puml_count++; + + fs.readFile(href.replace("./", renderer._in_folder)).then(puml => { + return cache_lookup(puml, () => plantuml(puml)) + //return plantuml(puml) + }).then(svg => { + fs.writeFile(renderer._out_folder + "puml_" + i + ".svg", svg) + }) + + return `${text}`; + + } + + if (href.endsWith(".fml")) { + + let i = fml_count++; + + const fml_output = renderer._out_folder + "fml_" + i + ".svg"; + + run("node fml " + href.replace("./", renderer._in_folder) + " " + fml_output); + + return `${text}`; + + } + + if (href.endsWith(".c5")) { + + let i = fml_count++; + + const fml_output = renderer._out_folder + "c5_" + i + ".svg"; + + run("node c5 " + href.replace("./", renderer._in_folder) + " " + fml_output); + + return `${text}`; + + } + + if (href.endsWith(".madr")) { + + let md = fs.readFileSync(href.replace("./", renderer._in_folder)); + + return `

${text}

` + marked.parse("" + new String(md)); + + } + + return renderer._image(href, title, text); + +} + +renderer._heading = renderer.heading; + +let task_count = 0; + +renderer.heading = function (text, level, raw, slugger) { + + // console.log(text); + + if (text.indexOf("Ex -") >= 0) { + task_count++; + text = text.replace("Ex -", `${task_count}. `); + } + + // let sec = "
" + + // if (task_count > 1) { + // sec = "
" + sec; + // } + + return renderer._heading(text, level, raw, slugger); +}; + + +marked.setOptions({ + renderer, //: new marked.Renderer(), + highlight: function (code) { + return code; //require('highlight').highlightAuto(code).value; + }, + pedantic: false, + gfm: true, + breaks: false, + sanitize: false, + smartLists: true, + smartypants: false, + xhtml: false +}); + +function loadMD(f, vars = {}) { + + let txt = "" + new String(fs.readFileSync(f)); + + if (txt.indexOf("---") >= 0) { + + let parts = txt.split("\n---\n"); + + parts.slice(0, -1).forEach(p => { + let lines = p.split("\n"); + let k = lines[0].split(":"); + vars[k[0]] = lines.slice(1).join("\n"); + }); + + txt = parts[parts.length - 1]; + + } + + //convert strings to booleans + Object.keys(vars).forEach(k => { + if (vars[k] === "false") vars[k] = false; + if (vars[k] === "true") vars[k] = true; + }) + + console.log(vars); + + task_count = 0; + + return { md: marked.parse(txt), meta: vars, toc: toc(txt) }; +} + + +let ejs = require('ejs'); + +let views = {} + +function render_one(model, out_file, template) { + + model.out_file = out_file; + + views[template] = views[template] || new String(fs.readFileSync("./template/" + template + ".ejs")); + + let html = ejs.render(views[template], model); + + fs.ensureFileSync(out_file); + fs.writeFileSync(out_file, html); + +}; + + +var walk = function (dir, done) { + var results = []; + fs.readdir(dir, function (err, list) { + if (err) return done(err); + var i = 0; + (function next() { + var file = list[i++]; + if (!file) return done(null, results); + file = dir + '/' + file; + fs.stat(file, function (err, stat) { + if (stat && stat.isDirectory()) { + walk(file, function (err, res) { + results = results.concat(res); + next(); + }); + } else { + results.push({ name: file, stat }); + next(); + } + }); + })(); + }); +}; + + + +function deltaBuild() { + let old_file_list = {}; + try { + old_file_list = fs.readJsonSync('upload/files.json'); + } catch (e) { } + + walk("src", function (err, list) { + + if (list === undefined) { + console.log("Missing src folder") + return; + } + + if (list.length == 0) { + console.log("Empty src folder") + return; + } + + let flat = {}; + list.filter((f) => !f.name.endsWith(".DS_Store")).forEach(f => { + flat[f.name] = f.stat; + }); + + let todo = {}; + + let deleted = {}; + Object.keys(old_file_list).forEach(k => { deleted[k] = old_file_list[k] }); + + let changed = []; + + function extract_dir(k) { + let a = k.split("/") + return a.slice(1, 3).join("/"); + } + + function push(k) { + let d = extract_dir(k); + todo[d] = (todo[d] || 0) + 1; + + changed.push(k); + } + + Object.keys(flat).forEach((k) => { + if (old_file_list[k]) { + if (flat[k].mtimeMs != old_file_list[k].mtimeMs) { + push(k); + } + delete deleted[k]; //k was found so what's left over should have been deleted + } else { + push(k); //this is a new addition + } + }); + + fs.writeJson("upload/files.json", flat); + + console.log('Changed', changed); + console.log('Deleted', Object.keys(deleted)); + + if (Object.keys(todo).length == 0) { + todo['sa/model'] = 1; + } + + //todo['exercises/8-ws'] = 1; + + if (Object.keys(todo).indexOf('templates/doc.ejs') != -1) { + todo = Array.from(new Set(Object.keys(flat).map(extract_dir))).reduce((a, c) => { a[c] = 1; return a; }, {}); + delete todo['templates/doc.ejs']; + } + + Object.keys(todo).filter(k => k.endsWith('meta.json')).forEach(mk => { + delete todo[mk]; + Array.from(new Set(Object.keys(flat).map(extract_dir))).filter(k => k.startsWith(mk.split("/")[0])).forEach(k => { + todo[k] = 1; + }) + }) + + console.log("Building", todo); + + Object.keys(todo).forEach(buildDoc.bind(null, changed, Object.keys(deleted))); + + }) +} + +function buildDoc(changed, deleted, slide) { + + console.log("Building " + slide); + + let md_path = "./src/" + slide + "/index.md"; + + let md = { meta: {} }; + + let meta_path = "./src/" + slide.split("/")[0] + "/meta.json"; + try { + md.meta = fs.readJSONSync(meta_path); + } catch (e) { + //console.log(e); + } + + md.meta.build = build_json.doc_build; + //md.meta.lecture = "Web Atelier 2020 "; + md.meta.timestamp = new Date().toLocaleString(); + + if (fs.existsSync(md_path) && fs.lstatSync(md_path).isFile()) { + + renderer._out_folder = `./upload/${slide}/`; + renderer._in_folder = `./src/${slide}/`; + + md = loadMD(md_path, md.meta); + + md.meta.series = ""; + + let model = { title: slide, md: md.md, toc: md.toc, meta: md.meta, pid: slide + "." + build_json.build } + + let mdout = `./upload/${slide}/index.html`; + + render_one(model, mdout, 'doc'); + + } + +} + +fs.ensureDirSync("upload/"); +deltaBuild(); \ No newline at end of file diff --git a/c5.js b/c5.js new file mode 100644 index 0000000..749f9d2 --- /dev/null +++ b/c5.js @@ -0,0 +1 @@ +"use strict";var t=require("fs-extra"),e=require("dagrejs"),r=require("svgdom"),n=require("@svgdotjs/svg.js");function l(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var o=l(t),a=l(e),i=l(r),c=l(n);function s(t,e,r,n){this.message=t,this.expected=e,this.found=r,this.location=n,this.name="SyntaxError","function"==typeof Error.captureStackTrace&&Error.captureStackTrace(this,s)}!function(t,e){function r(){this.constructor=t}r.prototype=e.prototype,t.prototype=new r}(s,Error),s.buildMessage=function(t,e){var r={literal:function(t){return'"'+l(t.text)+'"'},class:function(t){var e,r="";for(e=0;e0){for(e=1,n=1;e",u=W("->",!1),f="<-",m=W("<-",!1),d="<->",p=W("<->",!1),b="-",g=W("-",!1),v=W("'",!1),y=W('"',!1),k=/^[^']/,w=D(["'"],!0,!1),x=function(){return t.substring(Z,R)},A=/^[^"]/,z=D(['"'],!0,!1),E=/^[a-zA-Z0-9\/._ ]/,_=D([["a","z"],["A","Z"],["0","9"],"/",".","_"," "],!1,!1),C=/^[a-zA-Z0-9\/._]/,F=D([["a","z"],["A","Z"],["0","9"],"/",".","_"],!1,!1),S=H("whitespace"),j=/^[ \t\n\r]/,q=D([" ","\t","\n","\r"],!1,!1),N=H("eol"),G=/^[\n\r]/,M=D(["\n","\r"],!1,!1),R=0,Z=0,O=[{line:1,column:1}],T=0,U=[],V=0;if("startRule"in e){if(!(e.startRule in i))throw new Error("Can't start parsing from rule \""+e.startRule+'".');c=i[e.startRule]}function W(t,e){return{type:"literal",text:t,ignoreCase:e}}function D(t,e,r){return{type:"class",parts:t,inverted:e,ignoreCase:r}}function H(t){return{type:"other",description:t}}function J(e){var r,n=O[e];if(n)return n;for(r=e-1;!O[r];)r--;for(n={line:(n=O[r]).line,column:n.column};rT&&(T=R,U=[]),U.push(t))}function I(){return function(){var t,e,r;t=R,e=[],r=K();for(;r!==a;)e.push(r),r=K();e!==a&&(Z=t,(n=e).forEach(((t,e)=>{""==t.from&&(t.from="backward"==n[e-1].type||"both"==n[e-1].type?n[e-1].from:n[e-1].to),""==t.to&&(t.to="backward"==n[e-1].type||"both"==n[e-1].type?n[e-1].from:n[e-1].to)})),e={edges:n});var n;return t=e}()}function K(){var e;return(e=function(){var e;(e=function(){var e,r,n,l,o,i;e=R,(r=P())!==a&&X()!==a?(t.substr(R,2)===f?(n=f,R+=2):(n=a,0===V&&B(m)),n!==a&&(l=Q())!==a?(t.substr(R,2)===h?(o=h,R+=2):(o=a,0===V&&B(u)),o!==a&&X()!==a&&(i=P())!==a&&Y()!==a?(Z=e,c=r,s=l,e=r={from:i.trim(),to:c.trim(),type:"both",action:s}):(R=e,e=a)):(R=e,e=a)):(R=e,e=a);var c,s;return e}())===a&&(e=function(){var e,r,n,l,o,i;e=R,(r=P())!==a&&X()!==a?(45===t.charCodeAt(R)?(n="-",R++):(n=a,0===V&&B(g)),n!==a&&(l=Q())!==a?(t.substr(R,2)===h?(o=h,R+=2):(o=a,0===V&&B(u)),o!==a&&X()!==a&&(i=P())!==a&&Y()!==a?(Z=e,c=l,s=i,e=r={from:r.trim(),to:s.trim(),type:"forward",action:c}):(R=e,e=a)):(R=e,e=a)):(R=e,e=a);var c,s;return e}())===a&&(e=function(){var e,r,n,l,o,i;e=R,(r=P())!==a&&X()!==a?(t.substr(R,2)===f?(n=f,R+=2):(n=a,0===V&&B(m)),n!==a&&(l=Q())!==a?(45===t.charCodeAt(R)?(o=b,R++):(o=a,0===V&&B(g)),o!==a&&X()!==a&&(i=P())!==a&&Y()!==a?(Z=e,c=r,s=l,e=r={from:i.trim(),to:c.trim(),type:"backward",action:s}):(R=e,e=a)):(R=e,e=a)):(R=e,e=a);var c,s;return e}());return e}())===a&&(e=function(){var e;(e=function(){var e,r,n,l;e=R,(r=P())!==a&&X()!==a?(t.substr(R,3)===d?(n=d,R+=3):(n=a,0===V&&B(p)),n!==a&&X()!==a&&(l=P())!==a&&Y()!==a?(Z=e,o=r,e=r={from:l.trim(),to:o.trim(),type:"both",action:""}):(R=e,e=a)):(R=e,e=a);var o;return e}())===a&&(e=function(){var e,r,n,l;e=R,(r=P())!==a&&X()!==a?(t.substr(R,2)===h?(n=h,R+=2):(n=a,0===V&&B(u)),n!==a&&X()!==a&&(l=P())!==a&&Y()!==a?(Z=e,o=l,e=r={from:r.trim(),to:o.trim(),type:"forward",action:""}):(R=e,e=a)):(R=e,e=a);var o;return e}())===a&&(e=function(){var e,r,n,l;e=R,(r=P())!==a&&X()!==a?(t.substr(R,2)===f?(n=f,R+=2):(n=a,0===V&&B(m)),n!==a&&X()!==a&&(l=P())!==a&&Y()!==a?(Z=e,o=r,e=r={from:l.trim(),to:o.trim(),type:"backward",action:""}):(R=e,e=a)):(R=e,e=a);var o;return e}());return e}()),e}function P(){var e,r,n,l;return e=R,39===t.charCodeAt(R)?(r="'",R++):(r=a,0===V&&B(v)),r!==a?(n=function(){var e,r,n;e=R,r=[],k.test(t.charAt(R))?(n=t.charAt(R),R++):(n=a,0===V&&B(w));for(;n!==a;)r.push(n),k.test(t.charAt(R))?(n=t.charAt(R),R++):(n=a,0===V&&B(w));r!==a&&(Z=e,r=x());return e=r}(),n!==a?(39===t.charCodeAt(R)?(l="'",R++):(l=a,0===V&&B(v)),l!==a?(Z=e,e=r=n):(R=e,e=a)):(R=e,e=a)):(R=e,e=a),e===a&&(e=R,34===t.charCodeAt(R)?(r='"',R++):(r=a,0===V&&B(y)),r!==a?(n=function(){var e,r,n;e=R,r=[],A.test(t.charAt(R))?(n=t.charAt(R),R++):(n=a,0===V&&B(z));for(;n!==a;)r.push(n),A.test(t.charAt(R))?(n=t.charAt(R),R++):(n=a,0===V&&B(z));r!==a&&(Z=e,r=x());return e=r}(),n!==a?(34===t.charCodeAt(R)?(l='"',R++):(l=a,0===V&&B(y)),l!==a?(Z=e,e=r=n):(R=e,e=a)):(R=e,e=a)):(R=e,e=a),e===a&&(e=R,r=function(){var e,r,n;e=R,r=[],E.test(t.charAt(R))?(n=t.charAt(R),R++):(n=a,0===V&&B(_));for(;n!==a;)r.push(n),E.test(t.charAt(R))?(n=t.charAt(R),R++):(n=a,0===V&&B(_));r!==a&&(Z=e,r=x());return e=r}(),r!==a&&(Z=e,r=r),(e=r)===a&&(e=R,(r=Q())!==a&&(Z=e,r=r),e=r))),e}function Q(){var e,r,n;for(e=R,r=[],C.test(t.charAt(R))?(n=t.charAt(R),R++):(n=a,0===V&&B(F));n!==a;)r.push(n),C.test(t.charAt(R))?(n=t.charAt(R),R++):(n=a,0===V&&B(F));return r!==a&&(Z=e,r=x()),e=r}function X(){var e,r,n;for(V++,e=R,r=[],j.test(t.charAt(R))?(n=t.charAt(R),R++):(n=a,0===V&&B(q));n!==a;)r.push(n),j.test(t.charAt(R))?(n=t.charAt(R),R++):(n=a,0===V&&B(q));return r!==a&&(Z=e,r=void 0),V--,(e=r)===a&&(r=a,0===V&&B(S)),e}function Y(){var e,r,n;for(V++,e=R,r=[],G.test(t.charAt(R))?(n=t.charAt(R),R++):(n=a,0===V&&B(M));n!==a;)r.push(n),G.test(t.charAt(R))?(n=t.charAt(R),R++):(n=a,0===V&&B(M));return r!==a&&(Z=e,r=void 0),V--,(e=r)===a&&(r=a,0===V&&B(N)),e}if((r=c())!==a&&R===t.length)return r;throw r!==a&&Rt.path("m 0 0 v 20 h 20 v -2.0098 h -18.0449 v -17.9902 z m 1.9626 -2.028 h 15.8422 l -0.0552 4.212 l 4.213 -0.0302 v 15.8182 h -20 z m 15.873 0.021 l 4.126 4.104").fill("white").stroke("black").center(e.x,e.y),file:(t,e)=>t.path("m 0 0 h 15.8422 l -0.0552 4.212 l 4.213 -0.0302 v 15.8182 h -20 z m 15.873 0.021 l 4.126 4.104").fill("white").stroke("black").center(e.x,e.y),db:(t,e)=>t.path("m 0 0 c -0.032 -3.3343 20.0253 -2.8621 20.044 -0.1028 c -0.0761 4.4369 -20.1432 4.2433 -20.044 0.1028 z m 0.0294 0.6461 c 2.1507 3.0603 17.3947 3.3723 20 0 v 20 c -6.5332 2.6684 -13.1865 2.9358 -20 0 z").fill("white").stroke("black").center(e.x,e.y),web:(t,e)=>t.path("m 0 0 c 5.4735 1.7589 10.947 1.755 16.4205 -0.0113 m -15.7172 -10.497 c 5.615 1.458 11.045 1.368 16.151 0.073 m -17.9251 5.3549 c 6.5771 1.7119 12.9818 1.2983 19.3139 -0.0113 m 0 0 c 0 5.5228 -4.1082 9.6311 -9.6311 9.6311 c -5.5228 0 -9.6618 -4.1082 -9.6618 -9.6311 c 0 -5.5228 4.139 -9.6003 9.6618 -9.6003 c 5.5229 0 9.6311 4.0775 9.6311 9.6003 z").fill("white").stroke("black").center(e.x,e.y),tuple:(t,e)=>t.path("m 0 0 l 4.5266 -3.9241 m -20.021 3.9241 l 3.9352 -3.7814 l 16.0859 -0.1427 l 0.0542 16.5903 l -4.5808 3.3203 m -15.4945 -15.9864 h 15.4945 v 15.9864 h -15.4945 z").fill("white").stroke("black").center(e.x,e.y),ram:(t,e)=>t.path("m 0 0 l -3.9351 0.0615 m 4.0274 -2.8438 l -3.9351 0.0615 m 3.9351 -2.9053 l -3.9351 0.0615 m 3.9351 -2.9052 l -3.9351 0.0615 m 3.9351 -2.9052 l -3.9351 0.0615 m 3.9351 -2.9052 l -3.9351 0.0615 m 3.9659 -2.8438 l -3.9351 0.0615 m 23.8874 16.9087 l -3.9351 0.0615 m 3.9351 -2.8745 l -3.9351 0.0615 m 3.9351 -2.9053 l -3.9351 0.0615 m 3.9351 -2.9052 l -3.9351 0.0615 m 3.9351 -2.9052 l -3.9351 0.0615 m 3.9351 -2.9052 l -3.9351 0.0615 m 3.9351 -2.8745 l -3.9351 0.0615 m 0.3997 16.4783 l 0.0615 3.9351 m -2.9052 -3.9351 l 0.0615 3.9351 m -2.9053 -3.9351 l 0.0615 3.9351 m -2.9052 -3.9351 l 0.0615 3.9351 m -2.9052 -3.9351 l 0.0615 3.9351 m -2.9052 -3.9351 l 0.0615 3.9351 m -2.9053 -3.9351 l 0.0615 3.9351 m 17.0317 -23.8567 l 0.0615 3.9351 m -2.9052 -3.9351 l 0.0615 3.9351 m -2.9053 -3.9351 l 0.0615 3.9351 m -2.9052 -3.9351 l 0.0615 3.9351 m -2.9052 -3.9351 l 0.0615 3.9351 m -2.9052 -3.9351 l 0.0615 3.9351 m -2.9053 -3.9351 l 0.0615 3.9351 m -1.3624 -1.9489 h 20 v 20 h -20 z").fill("none").stroke("black").center(e.x,e.y),msg:(t,e)=>t.path("m 0 0 l 9.8378 10.5449 l 9.8378 -10.8523 m -19.93 -0.008 h 20 v 20 h -20 z").center(e.x,e.y).fill("white").stroke("black"),bus:(t,e)=>t.path("m 0 0 l 41.0532 0.048 c 2.6625 4.4517 2.8554 9.081 0 13.7732 c -13.4752 0.0145 -25.0265 -0.2369 -40.3397 -0.0385 c -6.0837 -0.003 -5.2939 -13.7927 -0.6801 -13.7519 c 4.2341 -0.0998 4.2921 13.6962 0.6777 13.7462").center(e.x,e.y).fill("white").stroke("black"),disruptor:(t,e)=>t.path("m 0 0 c -1.1712 0.0939 -2.3376 0.1775 -3.4239 0.544 m -0.0397 -0.6622 l 2.7894 -0.1145 m -4.9707 2.4926 l 2.2736 -3.2136 l 4.9549 0.5181 m -5.7642 -1.1189 l 6.3551 1.2145 m -5.7867 -1.3894 l -2.2399 4.3629 m -8.4983 4.602 l -2.8449 0.4742 m 4.7479 2.0174 l -2.8133 1.5173 m 5.184 0.2529 l -1.3592 2.5288 m 4.3306 -2.4024 l 0.2213 2.9081 m 2.4972 -4.0777 l 1.6437 2.3391 m 0.0316 -4.4254 l 3.1926 1.6437 m -2.5604 -4.0461 l 3.73 0.5058 m -3.8564 -3.6035 l 3.3823 -0.5374 m -3.2641 2.6637 c 0 3.8344 -2.6139 6.6867 -6.128 6.6867 c -3.514 0 -6.1475 -2.8523 -6.1475 -6.6867 c 0 -3.8344 2.6335 -6.6653 6.1475 -6.6653 c 3.514 0 6.128 2.8309 6.128 6.6653 z m 3.6984 0.0269 c 0 5.5228 -4.1082 9.6311 -9.6311 9.6311 c -5.5228 0 -9.6618 -4.1082 -9.6618 -9.6311 c 0 -5.5229 4.139 -9.6003 9.6618 -9.6003 c 5.5229 0 9.6311 4.0775 9.6311 9.6003 z").center(e.x,e.y).fill("white").stroke("black"),blockchain:(t,e)=>t.path("m 0 0 v 2.8457 h 11.4707 v -20 h -11.4707 v 1.7504 m 0 1.9239 v 2.5118 m 0 1.8705 v 2.1377 m 0 2.2179 v 2.4725 m 25.8837 -8.321 h 2.9931 c 1.5112 0 2.7278 1.2166 2.7278 2.7278 v 1.1034 c 0 1.5112 -1.2166 2.7279 -2.7278 2.7279 h -2.9931 c -1.5112 0 -2.7279 -1.2166 -2.7279 -2.7279 v -1.1034 c 0 -1.5112 1.2166 -2.7278 2.7279 -2.7278 z m -14.4694 -0.1186 h 2.9931 c 1.5112 0 2.7278 1.2166 2.7278 2.7278 v 1.1034 c 0 1.5112 -1.2166 2.7278 -2.7278 2.7278 h -2.9931 c -1.5112 0 -2.7278 -1.2166 -2.7278 -2.7278 v -1.1034 c 0 -1.5112 1.2166 -2.7278 2.7278 -2.7278 z m 2.8765 -6.5636 h 11.4724 v 20 h -11.4724 z m 14.7659 0 h 11.52 v 20 h -11.52 z").center(e.x,e.y).fill("none").stroke("black"),test:(t,e)=>t.rect(e.width,e.height).center(e.x,e.y).fill("red").stroke("black")};let w=process.argv[2],x=process.argv[3],A=process.argv[4];if(null!=w&&null!=x||console.log("Usage: node "+process.argv[1]+" [input.c5] [output.svg] [FONT]"),x=x||"output.svg",w=w||"input.c5",A=A||"Helvetica",u.existsSync(w))try{let E=u.readFileSync(w),_=m.parse((z=""+E,z.split("\n").map((t=>t.split("///")[0])).join("\n")).trim());v(p,b),function(t,e){let r=0,n={};function l(t){return n[t]||(r++,n[t]="n"+r),n[t]}t.edges.forEach((function(t){t.from_id=l(t.from),t.to_id=l(t.to)})),Object.keys(n).forEach((t=>{let r=t;e.setNode(n[t],{label:r,width:10*r.length+30,height:40})})),t.edges.forEach((function(t){if("file"==t.action){let r=t.from_id+"-"+t.to_id;e.setNode(r,{width:20,height:20,icon:"file",label:""}),e.setEdge(t.from_id,r,{action:t.action+" write"}),e.setEdge(r,t.to_id,{action:t.action+" read"})}else if("queue"==t.action){let r=t.from_id+"-"+t.to_id;e.setNode(r,{width:20,height:20,icon:"msg",label:""}),e.setEdge(t.from_id,r,{action:t.action+" write"}),e.setEdge(r,t.to_id,{action:t.action+" read"})}else e.setEdge(t.from_id,t.to_id,{action:t.action,label:t.action,type:t.type})}))}(_,y),function(t){const e=g(b.documentElement);t.nodes().forEach((r=>{let n=t.node(r),l=n.label.trim().split(" ")[0];if(n.original_label=n.label,n.connector_label=n.label.trim().split(" ").slice(1).join(" "),"db"==l&&(n.icon="db",n.label="",n.width=20,n.height=20),"files"==l&&(n.icon="files",n.label="",n.width=22,n.height=22),"web"==l&&(n.icon="web",n.label="",n.width=20,n.height=20),"tuple"==l&&(n.icon="tuple",n.label="",n.width=20,n.height=20),"ram"==l&&(n.icon="ram",n.label="",n.width=20,n.height=20),"bus"==l&&(n.icon="bus",n.label="",n.width=40,n.height=12),"queue"==l&&(n.icon="msg",n.label="",n.width=20,n.height=20),"disruptor"==l&&(n.icon="disruptor",n.label="",n.width=20,n.height=20),"blockchain"==l&&(n.icon="blockchain",n.label="",n.width=40,n.height=20),n.label&&n.label.length>0){let t=e.text(n.label).font({family:A,size:12}).bbox();n.width=t.width+20,n.height=t.height+10}})),e.clear()}(y),f.layout(y);const C=g(b.documentElement);function F(t,e){var r,n,l=t.x,o=t.y,a=e.x-l,i=e.y-o,c=t.width/2,s=t.height/2;return Math.abs(i)*c>Math.abs(a)*s?(i<0&&(s=-s),r=0===i?0:s*a/i,n=s):(a<0&&(c=-c),r=c,n=0===a?0:c*i/a),{x:l+r,y:o+n}}function S(t,e){var r=t.edge(e),n=t.node(e.v),l=t.node(e.w),o=r.points.slice(1,r.points.length-1);return o.unshift(F(n,o[0])),o.push(F(l,o[o.length-1])),o}function j(t,e,r){if("stream"==e.action){let e=t.polyline(r.map((t=>[t.x,t.y]))).stroke({width:.25,color:"black"}).fill("none");return e.marker("end",100,40,(function(t){t.path("m 0 0 l 20 20 l -20 20 a 10 1 90 0 0 0 -40 z m 19.96 4.4 l 15.5 15.63 l -15.43 15.23 a 10 1 90 0 0 -0.07 -30.79 z m 16.12 5.51 l 10.34 10.12 l -10.27 10.2 a 10 1 90 0 0 -0.07 -20.32 z").fill("black").stroke({width:.1,color:"black"})})),e}if("call"==e.action){let e=t.polyline(r.map((t=>[t.x-2,t.y]))).stroke({width:.25,color:"black"}).fill("none");return e.marker("end",40,30,(function(t){t.path("m 0 0 l 20 15 l -20 15 a 7.5 1 90 0 0 0 -30 z").fill("black").stroke({width:.1,color:"black"})})),e=t.polyline(r.reverse().map((t=>[t.x+2,t.y]))).stroke({width:.25,color:"black"}).fill("none"),e.stroke({dasharray:"2"}),e.marker("end",40,30,(function(t){t.path("m 0 0 l 20 15 l -20 15 a 7.5 1 90 0 0 0 -30 z").fill("black").stroke({width:.1,color:"black"})})),e}if("both"==e.type){let e=t.polyline(r.map((t=>[t.x,t.y]))).stroke({width:.25,color:"black"}).fill("none");return e.marker("end",40,30,(function(t){t.path("m 0 0 l 20 15 l -20 15 a 7.5 1 90 0 0 0 -30 z").fill("black").stroke({width:.1,color:"black"})})),e.marker("start",40,30,(function(t){t.path("m 40 0 l -20 15 l 20 15 a 7.5 1 -90 0 1 0 -30 z").fill("black").stroke({width:.1,color:"black"})})),e}let n=t.polyline(r.map((t=>[t.x,t.y]))).stroke({width:.25,color:"black"}).fill("none");return n.marker("end",40,30,(function(t){t.path("m 0 0 l 20 15 l -20 15 a 7.5 1 90 0 0 0 -30 z").fill("black").stroke({width:.1,color:"black"})})),n}C.add(g("")),y.nodes().forEach((t=>{let e=y.node(t);if(k[e.icon]){let t=k[e.icon](C,e).bbox();if(e.connector_label){let r=C.text(e.connector_label).center(e.x+t.width,e.y).font({family:A,size:11}),n=r.bbox();r.dmove(2+n.width/2,-2)}}else if(C.rect(e.width,e.height).center(e.x,e.y).fill("white").stroke("black"),e.label){C.text(e.label).center(e.x+e.label.length,e.y-2).font({family:A,size:12}).bbox()}})),C.viewbox(C.bbox()),y.edges().forEach((t=>{var e=y.edge(t);let r=C.group(),n=j(r,e,S(y,t));if(e.label&&e.label.length>0&&"call"!=e.action&&"stream"!=e.action){let t=n.bbox(),l=r.text(e.label).center((t.x+t.x2)/2,(t.y+t.y2)/2).font({family:A,size:10}).addClass("label"),o=l.bbox();l.dmove(o.width/2,0),0==t.width&&l.transform({rotate:90})}})),"-"==x?console.info(C.svg()):u.writeFile(x,C.svg())}catch(q){console.error(q.message),console.error(JSON.stringify(q,null,2)),process.exit(1)}else console.error(w+" does not exist"),process.exit(2);var z;module.exports={}; diff --git a/fml.js b/fml.js new file mode 100644 index 0000000..cc0cbf4 --- /dev/null +++ b/fml.js @@ -0,0 +1 @@ +"use strict";var e=require("fs-extra"),r=require("dagrejs"),t=require("svgdom"),n=require("@svgdotjs/svg.js");function o(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var a=o(e),i=o(r),l=o(t),u=o(n);function c(e,r,t,n){this.message=e,this.expected=r,this.found=t,this.location=n,this.name="SyntaxError","function"==typeof Error.captureStackTrace&&Error.captureStackTrace(this,c)}!function(e,r){function t(){this.constructor=e}t.prototype=r.prototype,e.prototype=new t}(c,Error),c.buildMessage=function(e,r){var t={literal:function(e){return'"'+o(e.text)+'"'},class:function(e){var r,t="";for(r=0;r0){for(r=1,n=1;r",f=H("=>",!1),d="<=",p=H("<=",!1),h="<--\x3e",g=H("<--\x3e",!1),m=H(":",!1),v=H("{",!1),x=",",y=H(",",!1),b=H("}",!1),A=H("(",!1),k=H(")",!1),w=H("[",!1),E=H("]",!1),C="[ ]",q=H("[ ]",!1),F="[]",S=H("[]",!1),M="[*]",j=H("[*]",!1),G=/^[a-zA-Z0-9\/._\- ]/,R=I([["a","z"],["A","Z"],["0","9"],"/",".","_","-"," "],!1,!1),U=function(){return e.substring(_,Z)},W=K("whitespace"),z=/^[ \t\n\r]/,L=I([" ","\t","\n","\r"],!1,!1),N=K("eol"),T=/^[\n\r]/,V=I(["\n","\r"],!1,!1),Z=0,_=0,D=[{line:1,column:1}],J=0,O=[],B=0;if("startRule"in r){if(!(r.startRule in l))throw new Error("Can't start parsing from rule \""+r.startRule+'".');u=l[r.startRule]}function H(e,r){return{type:"literal",text:e,ignoreCase:r}}function I(e,r,t){return{type:"class",parts:e,inverted:r,ignoreCase:t}}function K(e){return{type:"other",description:e}}function P(r){var t,n=D[r];if(n)return n;for(t=r-1;!D[t];)t--;for(n={line:(n=D[t]).line,column:n.column};tJ&&(J=Z,O=[]),O.push(e))}function Y(){var e,r,t;return e=Z,(r=ee())!==i?(t=function(){var e,r,t;e=Z,r=[],t=$();for(;t!==i;)r.push(t),t=$();r!==i&&(_=e,(n=r).forEach(((e,r)=>{""==e.from&&(e.from="rimply"==n[r-1].type?n[r-1].from:n[r-1].to),""==e.to&&(e.to="rimply"==n[r-1].type?n[r-1].from:n[r-1].to)})),r={imply:n.filter((e=>"imply"==e.type||"rimply"==e.type)),exclude:n.filter((e=>"exclude"==e.type))});var n;return e=r}(),t!==i?e=r=[r,t]:(Z=e,e=i)):(Z=e,e=i),e}function $(){var r;return(r=function(){var r,t,n,o;r=Z,(t=te())!==i&&ne()!==i?(e.substr(Z,4)===h?(n=h,Z+=4):(n=i,0===B&&X(g)),n!==i&&ne()!==i&&(o=te())!==i&&oe()!==i?(_=r,a=o,r=t={from:t.trim(),to:a.trim(),type:"exclude"}):(Z=r,r=i)):(Z=r,r=i);var a;return r}())===i&&(r=function(){var r,t,n,o;r=Z,(t=te())!==i&&ne()!==i?(e.substr(Z,2)===s?(n=s,Z+=2):(n=i,0===B&&X(f)),n!==i&&ne()!==i&&(o=te())!==i&&oe()!==i?(_=r,a=o,r=t={from:t.trim(),to:a.trim(),type:"imply"}):(Z=r,r=i)):(Z=r,r=i);var a;return r}())===i&&(r=function(){var r,t,n,o;r=Z,(t=te())!==i&&ne()!==i?(e.substr(Z,2)===d?(n=d,Z+=2):(n=i,0===B&&X(p)),n!==i&&ne()!==i&&(o=te())!==i&&oe()!==i?(_=r,a=t,r=t={from:o.trim(),to:a.trim(),type:"rimply"}):(Z=r,r=i)):(Z=r,r=i);var a;return r}()),r}function ee(){var r;return(r=function(){var r,t,n,o;r=Z,(t=re())!==i?(58===e.charCodeAt(Z)?(n=":",Z++):(n=i,0===B&&X(m)),n!==i?(o=function(){var r,t,n,o,a,l,u,c;if(r=Z,(t=ne())!==i)if(123===e.charCodeAt(Z)?(n="{",Z++):(n=i,0===B&&X(v)),n!==i)if(ne()!==i)if((o=ee())!==i){for(a=[],l=Z,44===e.charCodeAt(Z)?(u=x,Z++):(u=i,0===B&&X(y)),u!==i&&(c=ee())!==i?l=u=[u,c]:(Z=l,l=i);l!==i;)a.push(l),l=Z,44===e.charCodeAt(Z)?(u=x,Z++):(u=i,0===B&&X(y)),u!==i&&(c=ee())!==i?l=u=[u,c]:(Z=l,l=i);a!==i&&(l=ne())!==i?(125===e.charCodeAt(Z)?(u="}",Z++):(u=i,0===B&&X(b)),u!==i&&(c=ne())!==i?(_=r,r=t={exclusive:!0,list:[o].concat(a).flat().filter((e=>","!=e))}):(Z=r,r=i)):(Z=r,r=i)}else Z=r,r=i;else Z=r,r=i;else Z=r,r=i;else Z=r,r=i;if(r===i){if(r=Z,(t=ne())!==i)if(40===e.charCodeAt(Z)?(n="(",Z++):(n=i,0===B&&X(A)),n!==i)if(ne()!==i)if((o=ee())!==i){for(a=[],l=Z,44===e.charCodeAt(Z)?(u=x,Z++):(u=i,0===B&&X(y)),u!==i&&(c=ee())!==i?l=u=[u,c]:(Z=l,l=i);l!==i;)a.push(l),l=Z,44===e.charCodeAt(Z)?(u=x,Z++):(u=i,0===B&&X(y)),u!==i&&(c=ee())!==i?l=u=[u,c]:(Z=l,l=i);a!==i&&(l=ne())!==i?(41===e.charCodeAt(Z)?(u=")",Z++):(u=i,0===B&&X(k)),u!==i&&(c=ne())!==i?(_=r,t=function(e,r){return{exclusive:!1,list:[e].concat(r).flat().filter((e=>","!=e))}}(o,a),r=t):(Z=r,r=i)):(Z=r,r=i)}else Z=r,r=i;else Z=r,r=i;else Z=r,r=i;else Z=r,r=i;if(r===i)if(r=Z,(t=ne())!==i)if(91===e.charCodeAt(Z)?(n="[",Z++):(n=i,0===B&&X(w)),n!==i)if(ne()!==i)if((o=ee())!==i){for(a=[],l=Z,44===e.charCodeAt(Z)?(u=x,Z++):(u=i,0===B&&X(y)),u!==i&&(c=ee())!==i?l=u=[u,c]:(Z=l,l=i);l!==i;)a.push(l),l=Z,44===e.charCodeAt(Z)?(u=x,Z++):(u=i,0===B&&X(y)),u!==i&&(c=ee())!==i?l=u=[u,c]:(Z=l,l=i);a!==i&&(l=ne())!==i?(93===e.charCodeAt(Z)?(u="]",Z++):(u=i,0===B&&X(E)),u!==i&&(c=ne())!==i?(_=r,t=function(e,r){return{exclusive:void 0,list:[e].concat(r).flat().filter((e=>","!=e))}}(o,a),r=t):(Z=r,r=i)):(Z=r,r=i)}else Z=r,r=i;else Z=r,r=i;else Z=r,r=i;else Z=r,r=i}return r}(),o!==i?(_=r,l=o,r=t={required:(a=t).required,label:a.label.trim(),exclusive:l.exclusive,features:l.list}):(Z=r,r=i)):(Z=r,r=i)):(Z=r,r=i);var a,l;return r}())===i&&(r=re()),r}function re(){var r,t,n;return r=Z,ne()!==i?(t=function(){var r,t;r=function(){var r,t;r=Z,e.substr(Z,3)===M?(t=M,Z+=3):(t=i,0===B&&X(j));t!==i&&(_=r,t=!0);return r=t}(),r===i&&(r=function(){var r,t;r=Z,e.substr(Z,3)===C?(t=C,Z+=3):(t=i,0===B&&X(q));t!==i&&(_=r,t=!1);return r=t}(),r===i&&(r=function(){var r,t;r=Z,e.substr(Z,2)===F?(t=F,Z+=2):(t=i,0===B&&X(S));t!==i&&(_=r,t=!1);return r=t}(),r===i&&(r=Z,(t=ne())!==i&&(_=r,t=void 0),r=t)));return r}(),t!==i&&ne()!==i&&(n=te())!==i&&ne()!==i?(_=r,r={required:t,label:n.trim()}):(Z=r,r=i)):(Z=r,r=i),r}function te(){var r,t,n;for(r=Z,t=[],G.test(e.charAt(Z))?(n=e.charAt(Z),Z++):(n=i,0===B&&X(R));n!==i;)t.push(n),G.test(e.charAt(Z))?(n=e.charAt(Z),Z++):(n=i,0===B&&X(R));return t!==i&&(_=r,t=U()),r=t}function ne(){var r,t,n;for(B++,r=Z,t=[],z.test(e.charAt(Z))?(n=e.charAt(Z),Z++):(n=i,0===B&&X(L));n!==i;)t.push(n),z.test(e.charAt(Z))?(n=e.charAt(Z),Z++):(n=i,0===B&&X(L));return t!==i&&(_=r,t=void 0),B--,(r=t)===i&&(t=i,0===B&&X(W)),r}function oe(){var r,t,n;for(B++,r=Z,t=[],T.test(e.charAt(Z))?(n=e.charAt(Z),Z++):(n=i,0===B&&X(V));n!==i;)t.push(n),T.test(e.charAt(Z))?(n=e.charAt(Z),Z++):(n=i,0===B&&X(V));return t!==i&&(_=r,t=void 0),B--,(r=t)===i&&(t=i,0===B&&X(N)),r}if((t=u())!==i&&Z===e.length)return t;throw t!==i&&Ze.split("///")[0])).join("\n"));!function(e,r){let t={};!function e(r){r.features&&r.features.forEach(e),r.feat&&(r.features=r.feat.map((e=>({name:e}))))}(e.root);let n=0;!function e(o){let a=o.name||o.label;o.id="f"+n,t[a]=o.id,r.setNode(o.id,{label:a,width:10*a.length+30,height:40,required:o.required,exclusive:o.exclusive}),n++,o.features&&o.features.forEach(e)}(e.root),function e(t){t.features&&t.features.forEach((n=>{e(n),r.setEdge(t.id,n.id,{type:"feature"})}))}(e.root),e.required.forEach((e=>{let n=t[e.from],o=t[e.to];n&&o?r.setEdge(n,o,{type:"required"}):console.error("Warning: Unknown Feature: "+(null==n?"! "+e.from+" !":e.from)+" => "+(null==o?"! "+e.to+" !":e.to))})),e.exclude.forEach((e=>{let n=t[e.from],o=t[e.to];n&&o?r.setEdge(n,o,{type:"exclude"}):console.error("Warning: Unknown Feature: "+(null==n?"! "+e.from+" !":e.from)+" <--\x3e "+(null==o?"! "+e.to+" !":e.to))}))}({root:r[0],required:r[1].imply,exclude:r[1].exclude},y),d.layout(y),x(g,m);const t=v(m.documentElement);function n(e,r){var t,n,o=e.x,a=e.y,i=r.x-o,l=r.y-a,u=e.width/2,c=e.height/2;return Math.abs(l)*u>Math.abs(i)*c?(l<0&&(c=-c),t=0===l?0:c*i/l,n=c):(i<0&&(u=-u),t=u,n=0===i?0:u*l/i),{x:o+t,y:a+n}}function o(e,r){var t=e.edge(r),o=e.node(r.v),a=e.node(r.w),i=t.points.slice(1,t.points.length-1);return i.unshift(n(o,i[0])),i.push(n(a,i[i.length-1])),i}function a(e,r){return e.sy&&(r[0].y=e.sy),t.polyline(r.map((e=>[e.x,e.y]))).stroke("black").fill("none")}y.nodes().forEach((e=>{let r=y.node(e);t.rect(r.width,r.height).center(r.x,r.y).fill("white").stroke("black");t.text(r.label).center(r.x,r.y).bbox()})),t.viewbox(t.bbox()),y.nodes().forEach((e=>{let r=y.outEdges(e),n=y.node(e);if(r.length>1&&void 0!==n.exclusive){let e=[];r.forEach((r=>{let t=o(y,r);e.push(t[0])}));let a=e.map((e=>e.x)).reduce(((e,r)=>Math.max(e,r))),i=e.map((e=>e.x)).reduce(((e,r)=>Math.min(e,r))),l=e.map((e=>e.y)).reduce(((e,r)=>Math.max(e,r)));e.map((e=>e.y)).reduce(((e,r)=>Math.min(e,r)));r.forEach((e=>{y.edge(e).sy=l+10})),t.polygon([a,l+10,(a+i)/2,l+1,i,l+10]).fill(n.exclusive?"white":"black").stroke("black")}})),y.edges().forEach((e=>{var r=y.edge(e);let t=a(r,o(y,e));var n=y.node(e.w);void 0!==n.required&&"feature"==r.type&&t.marker("end",14,14,(function(e){e.circle(10).center(7,7).fill(n.required?"black":"white").stroke({width:2,color:"black"})})),"required"==r.type&&(t.stroke({dasharray:"4"}),t.marker("end",14,14,(function(e){e.polygon([0,14,7,7,0,0,2,7]).fill("black").stroke({width:1,color:"black"})}))),"exclude"==r.type&&(t.stroke({dasharray:"4"}),t.marker("end",14,14,(function(e){e.polygon([0,14,7,7,0,0,2,7]).fill("black").stroke({width:1,color:"black"})})),t.marker("start",14,14,(function(e){e.polygon([14,0,7,7,14,14,12,7]).fill("black").stroke({width:1,color:"black"})})))})),"-"==A?console.info(t.svg()):f.writeFile(A,t.svg())}catch(e){console.error(e.name+" at line "+e.location.start.line+":"+e.location.start.column),console.error(e.message),console.error(JSON.stringify(e,null,2)),process.exit(1)}else console.error(b+" does not exist"),process.exit(2);module.exports={}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..a52940e --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "msde-2023-sa", + "version": "1.0.0", + "description": "Software Architecture Modeling Exercises", + "main": "build.js", + "author": "Cesare Pautasso", + "license": "MIT", + "dependencies": { + "@svgdotjs/svg.js": "^3.1.2", + "dagrejs": "^0.2.1", + "ejs": "^3.1.6", + "fs-extra": "^10.0.0", + "glob-fs": "^0.1.7", + "lodash": "^4.17.21", + "marked": "^4.0.12", + "metalsmith-plantuml": "^0.0.4", + "svgdom": "^0.1.10", + "uslug": "^1.0.4" + }, + "scripts": { + "build": "node build.js", + "clean": "rm -rf upload/*", + "watch": "onchange src/** -- yarn build" + } +} diff --git a/src/sa/model/decisions/decision-template.madr b/src/sa/model/decisions/decision-template.madr new file mode 100644 index 0000000..fd7f614 --- /dev/null +++ b/src/sa/model/decisions/decision-template.madr @@ -0,0 +1,36 @@ +## ADR #0 (Template) + +1. What did you decide? + +Give a short title of solved problem and solution + +2. What was the context for your decision? + +What is the goal you are trying to achieve? + +What are the constraints? + +What is the scope of your decision? Does it affect the entire architecture? + +3. What is the problem you are trying to solve? + +You may want to articulate the problem in form of a question. + +4. Which alternative options did you consider? + +List at least 3 options + +5. Which one did you choose? + +Pick one of the options as the outcome of your decision + +6. What is the main reason for that? + +List the positive consequences (pros) of your decision: + +* quality improvement +* satisfaction of external constraint + +If any, list the negative consequences (cons) + +* quality degradation \ No newline at end of file diff --git a/src/sa/model/examples/connector-view.c5 b/src/sa/model/examples/connector-view.c5 new file mode 100644 index 0000000..0e9c4c3 --- /dev/null +++ b/src/sa/model/examples/connector-view.c5 @@ -0,0 +1,49 @@ +/// The Connector View is a graph of components/connectors + +/// Simple 1-1 connectors are modeled on the edges + +Client -call-> Server + +Source -stream-> Filter -stream-> Sink + +Writer -file-> Reader + +Sender -queue-> Recipient + +/// N-M connectors are modeled as nodes with a given type + +/// the shared "db" connector (Shared is just a label) + +Business Logic -> db Shared -> Analytics + +/// the message "bus" (ESB is just a label) + +Service -> bus ESB -> Microservice + +bus ESB -> Nanoservice + +/// the "web" (API is just a label) + +Browser -> web API -> Web Server + +/// the shared "ram" memory (Buffer is just a label) + +Writer Thread -> ram Buffer -> Reader Thread + +/// the "disruptor" + +Producer Thread -> disruptor Buffer -> Consumer Thread + +disruptor Buffer -> Another Consumer Thread + +/// the "tuple" Space (Space is just a label) +/// note that it is possible to label the edges + +Producer -in-> tuple Space -out-> Consumer + +/// the "blockchain" + +Miner -> blockchain -> Smart Contract + +/// Edges can flow in either or both directions +/// -> <- <-> diff --git a/src/sa/model/examples/context.puml b/src/sa/model/examples/context.puml new file mode 100644 index 0000000..542bba1 --- /dev/null +++ b/src/sa/model/examples/context.puml @@ -0,0 +1,16 @@ +@startuml +!include + +Person(user_s, "Students", "") +Person(user_p, "Profs", "") + +System_Boundary(boundary, "ASQ") { + +} + +System_Ext(icorsi, "iCorsi") + +Rel(user_s, boundary, "Answer questions") +Rel(user_p, boundary, "Ask questions") +Rel(boundary, icorsi, "Import/Export", "Student List, Grades") +@enduml \ No newline at end of file diff --git a/src/sa/model/examples/feature.fml b/src/sa/model/examples/feature.fml new file mode 100644 index 0000000..7a76a35 --- /dev/null +++ b/src/sa/model/examples/feature.fml @@ -0,0 +1,33 @@ + +/// First the Feature Tree + +/// Adding [*] before the feature name will make it Required +/// Adding [ ] before the feature name will mark it as Optional + +/// Composite features can express constraints between their sub-features +/// { } a set of exclusive features (pick one) +/// ( ) any combination of the features is possible +/// [ ] a set of features (usually combined with the [*] [ ] annotation) + +Product: [ + [*] Required Feature : { + Alternative Sub Feature1, + Alternative Sub Feature1 + }, + [ ] Optional Feature : [ + [*] Required Sub Feature, + [ ] Optional Sub Feature + ], + Combined Feature : ( + Sub Feature1, + Sub Feature2 + ) +] + +/// Then the feature relationships + +/// Implication +Sub Feature1 => Optional Sub Feature + +/// Exclusive +Alternative Sub Feature1 <--> Sub Feature2 diff --git a/src/sa/model/examples/feature.puml b/src/sa/model/examples/feature.puml new file mode 100644 index 0000000..46d32f7 --- /dev/null +++ b/src/sa/model/examples/feature.puml @@ -0,0 +1,46 @@ + +@startuml +rectangle "Feature" as A +rectangle "Optional" as B +rectangle "Required" as C + +A --0 B +A --@ C + +rectangle "Feature" as E +note left + Sub-features Combination +end note +rectangle "Sub Feature 1" as F +rectangle "Sub Feature 2" as G + +E *-- F +E *-- G + +rectangle "Feature" as H +note right + Exclusive Sub-features +end note + +rectangle "Alternative\nSubFeature 1" as I +rectangle "Alternative\nSubFeature 2" as J + +H o-- I +H o-- J + +F -[dotted]> B : require + + +C <-[dotted]> J : exclusive + +rectangle "Product" as R + +R --0 A +R --0 E : optional +R --@ H : required + + +skinparam monochrome true +skinparam shadowing false +skinparam defaultFontName Courier +@enduml diff --git a/src/sa/model/examples/interface1.puml b/src/sa/model/examples/interface1.puml new file mode 100644 index 0000000..badba2e --- /dev/null +++ b/src/sa/model/examples/interface1.puml @@ -0,0 +1,114 @@ +@startuml +hide circle + +class App { + +requires: +-- +operations: +.. +print(PDF) +print(id) +scheduleJob(PS) + +-- +properties: +.. +paper_size +color +duplex + +-- +events: +.. +print_started +print_done + + +} + + +class Driver { + +provides: +-- +operations: +.. +print(PDF) + +} + + +class Spooler { + +provides: +-- +operations: +.. +scheduleJob(PS) + +== + +requires: +-- +operations: +.. +post(PS) + +} + + +class Queue { + +provides: +-- +operations: +.. +post(PS) +get(id) +delete(id) + +} + +class Printer { + +provides: +-- +operations: +.. +print(id) + +-- +properties: +.. +paper_size +color +duplex + +-- +events: +.. +print_started +print_done + +== + +requires: +-- +operations: +.. +get(id) +delete(id) + +} + +App -(0- Driver +App -(0- Spooler +App -(0- Printer +Printer -(0- Queue +Spooler -(0- Queue + + +skinparam monochrome true +skinparam shadowing false +@enduml \ No newline at end of file diff --git a/src/sa/model/examples/interface2.puml b/src/sa/model/examples/interface2.puml new file mode 100644 index 0000000..b3e5525 --- /dev/null +++ b/src/sa/model/examples/interface2.puml @@ -0,0 +1,74 @@ +@startuml +component App +component Driver +interface " " as iDriver + +component Spooler +interface " " as iSpooler + +component Queue +interface " " as iQueue + +component Printer +interface " " as iPrinter + +App -( iDriver +iDriver - Driver + +App --( iSpooler +iSpooler - Spooler + +Spooler --( iQueue +iQueue - Queue + +Printer --( iQueue +iPrinter - Printer + +App --( iPrinter + + +note top of iDriver +operation: +.. +print(PDF) +end note + +note left of iSpooler +operation: +.. +scheduleJob(PS) +end note + +note left of iQueue +operations: +.. +post(PS) +get(id) +delete(id) +end note + + +note left of iPrinter +operation: +.. +print(id) + +-- +events: +.. +print_started +print_done + +-- +properties: +.. +paper_size +color +duplex +end note + + + +skinparam monochrome true +skinparam shadowing false +@enduml \ No newline at end of file diff --git a/src/sa/model/examples/wiki.fml b/src/sa/model/examples/wiki.fml new file mode 100644 index 0000000..863193f --- /dev/null +++ b/src/sa/model/examples/wiki.fml @@ -0,0 +1,10 @@ +E-Shop : [ + [*] Catalogue, + [*] Payment: ( + [*] Bank Transfer, [ ] Credit Card + ), + [*] Security: { + High, Standard + }, + [ ] Search +] diff --git a/src/sa/model/index.md b/src/sa/model/index.md new file mode 100644 index 0000000..6857aa1 --- /dev/null +++ b/src/sa/model/index.md @@ -0,0 +1,523 @@ +lecture: +Software Architecture +--- +title: +Model of My Project +--- +author: +Enter Your Full Name +--- + +# Getting started + +You will use [Markdown](https://www.markdownguide.org/cheat-sheetplan), [PlantUML](https://plantuml.com/), [architectural decision records](https://github.com/adr/madr), feature models and connector views to describe a software architecture model about your own project. + +This document will grow during the semester as you sketch and refine your software architecture model. + +When you are done with each task, please push so we can give you feedback about your work. + +We begin by selecting a suitable project domain. + + + +# Ex - Domain Selection + +{.instructions + +Submit the name and brief description (about 100 words) of your domain using the following vision statement template: + +``` +For [target customers] +Who [need/opportunity/problem] +The [name your project] +Is [type of project] +That [major features, core benefits, compelling reason to buy] +Unlike [current reality or competitors] +Our Project [summarize main advantages over status quo, unique selling point] +``` + +Please indicate if your choice is: + +* a project you have worked on in the past (by yourself or with a team) +* a project you are going to work on this semester in another lecture (which one?) +* a new project you plan to build in the future +* some existing open source project you are interested to contribute to + +The chosen domain should be unique for each student. + +Please be ready to give a 2 minute presentation about it (you can use one slide but it's not necessary) + +Hint: to choose a meaningful project look at the rest of the modeling tasks which you are going to perform in the context of your domain. + +} + +Project Name: *My Project* + +Project Type: + +Vision Statement: + +Additional Information: + + + +# Ex - Architectural Decision Records + +{.instructions + +Software architecture is about making design decisions that will impact the quality of the software you plan to build. + +Let's practice how to describe an architectural decision. We will keep using ADRs to document architectural decisions in the rest of the model. + +Use the following template to capture one or more architectural design decisions in the context of your project domain + +Pass: 1 ADR + +Good: 2 ADR + +Exceed: >2 ADR + +} + +![Architectural Decision Record Template](./decisions/decision-template.madr) + + +# Ex - Quality Attribute Scenario + +{.instructions + +1. Pick a scenario for a specific quality attribute. Describe it with natural language. + +2. Refine the scenario using the following structure: + +```puml +@startuml + +skinparam componentStyle rectangle +skinparam monochrome true +skinparam shadowing false + +rectangle Environment { + +[Source] -> [System] : Stimulus + +[System] -> [Measure] : Response + +} + +@enduml +``` + +*Stimulus*: condition affecting the system + +*Source*: entity generating the stimulus + +*Environment*: context under which stimulus occurred (e.g., build, test, deployment, startup, normal operation, overload, failure, attack, change) + +*Response*: observable result of the stimulus + +*Measure*: benchmark or target value defining a successful response + +Pass: 3 scenarios + +Good: >3 scenarios + +Exceed: >6 scenarios using challenging qualities + +} + +## Example Scenario + +Quality: _Recoverability_ + +Scenario: In case of power failure, rebooting the system should take up to 20 seconds. + +```puml +@startuml + +skinparam componentStyle rectangle +skinparam monochrome true +skinparam shadowing false + +rectangle "After Power has been restored" { + +rectangle "Admin" as Source +rectangle "max 20s" as Measure + +Source -> [System] : "Boot" + +[System] -> [Measure] : "Online" + +} + +@enduml +``` + + +# Ex - Quality Attribute Tradeoff + +{.instructions + +Pick a free combination of two qualities on the [map](https://usi365.sharepoint.com/:x:/s/MSDE2023-SoftwareArchitecture/EbK1lRTVOUZJhQoz0XdUBwIBd5vd5yQblOaOwYze4ovbuA?e=6aexs6) and write your name to claim it. + +Then write a short text giving an example for the tradeoff in this assignment. + +Pass: 1 unique trade-off + +Good: 2 trade-offs + +Exceed: >2 trade-offs + +} + +## Portability vs. Performance (Example) + +Developing an app natively for each OS is expensive and time consuming, but it benefits from a good performance. Choosing a cross-platform environment on the other hand simplify the development process, making it faster and cheaper, but it might suffer in performance. + +# Ex - Feature Modeling + +{.instructions + +In the context of your chosen project domain, describe your domain using a feature model. + +The feature model should be correctly visualized using the following template: + +![Example Feature Model Diagram](./examples/feature.puml) + +![Example Feature Model Diagram](./examples/feature.fml) + +If possible, make use of all modeling constructs. + +Pass: Include at least 4 non-trivial features + +Good: Include at least 6 non-trivial features, which are all implemented by your project + +Exceed: Include more than 8 non-trivial features, indicate which are found in your project and which belong to one competitor + +} + + +# Ex - Context Diagram + +{.instructions + +Prepare a context diagram to define the design boundary for your project. + +Here is a PlantUML/C4 example to get started. + +![Example Context Diagram](./examples/context.puml) + +Make sure to include all possible user personas and external dependencies you may need. + +Pass: 1 User and 1 Dependency + +Good: >1 User and >1 Dependency + +Exceed: >1 User and >1 Dependency, with both incoming and outgoing dependencies + +} + + + + +# Ex - Component Model: Top-Down + +{.instructions + +Within the context of your project domain, represent a model of your modular software architecture decomposed into components. + +The number of components in your logical view should be between 6 and 9: + +- At least one component should be further decomposed into sub components +- At least one component should already exist. You should plan how to reuse it, by locating it in some software repository and including in your model the exact link to its specification and its price. +- At least one component should be stateful. + +The logical view should represent provide/require dependencies that are consistent with the interactions represented in the process view. + +The process view should illustrate how the proposed decomposition is used to satisfy the main use case given by your domain model. + +You can add additional process views showing how other use cases can be satisfied by the same set of components. + +This assignment will focus on modularity-related decisions, we will worry about deployment and the container view later. + +Here is a PlantUML example logical view and process view. + +```puml +@startuml +skinparam componentStyle rectangle + +!include + +title Example Logical View +interface " " as MPI +interface " " as SRI +interface " " as CDI +interface " " as PSI +[Customer Database <$database{scale=0.33}>] as CDB +[Music Player] as MP +[User Interface] as UI +[Payment Service] as PS +[Songs Repository] as SR +MP - MPI +CDI - CDB +SRI -- SR +PSI -- PS +MPI )- UI +UI --( SRI +UI -( CDI +MP --( SRI +CDB --( PSI + + +skinparam monochrome true +skinparam shadowing false +skinparam defaultFontName Courier +@enduml +``` + +```puml +@startuml +title Example Process View: Buy and Play the Song + +participant "User Interface" as UI +participant "Music Player" as MP +participant "Songs Repository" as SR +participant "Customer Database" as CDB +participant "Payment Service" as PS + +UI -> SR: Browse Songs +UI -> CDB: Buy Song +CDB -> PS: Charge Customer +UI -> MP: Play Song +MP -> SR: Get Music + +skinparam monochrome true +skinparam shadowing false +skinparam defaultFontName Courier +@enduml +``` + +Hint: How to connect sub-components to other external components? Use this pattern. + +```puml +@startuml +component C { + component S + component S2 + S -(0- S2 +} +interface I +S - I + +component C2 +I )- C2 + +skinparam monochrome true +skinparam shadowing false +skinparam defaultFontName Courier +@enduml +``` + +Pass: 6 components (1 decomposed), 1 use case/process view + +Good: 6 components (1 decomposed), 2 use case/process view + +Exceed: >6 components (>1 decomposed) and >2 use case/process view + +} + +## Logical View + + + +## Process Views + +Use Case: + + + +# Ex - Component Model: Bottom-Up + +{.instructions + +Within the context of your project domain, represent a model of your modular software architecture decomposed into components. + +To design this model you should attempt to buy and reuse as many components as possible. + +In addition to the logical and process views, you should give a precise list to all sources and prices of the components you have selected to be reused. + +Write an ADR to document your component selection process (indicating which alternatives were considered). + +Pass: Existing design with at least 1 reused components (1 Logical View, 1 Process View) + +Good: Existing design with at least 3 reused components (1 Logical View, 1 Process View, 1 ADR) + +Exceed: Redesign based on >3 reused components (1 Logical View, >1 Process View, >1 ADR) + +} + + +# Ex - Interface/API Specification + +{.instructions + +In this iteration, we will detail your previous model to specify the provided interface of all components based on their interactions found in your existing process views. + +1. choose whether to use the top down or bottom up model. If you specify the interfaces of the bottom up model, your interface descriptions should match what the components you reuse already offer. + +2. decide which interface elements are operations, properties, or events. + +Get started with one of these PlantUML templates, or you can come up with your own notation to describe the interfaces, as long as it includes all the necessary details. + +The first template describes separately the provided/required interfaces of each component. + +![Separate Required/Provided Interfaces](./examples/interface1.puml) + +The second template annotates the logical view with the interface descriptions: less redundant, but needs the logical dependencies to be modeled to show which are the required interfaces. + +![Shared Interfaces](./examples/interface2.puml) + +Pass: define interfaces of all outer-level components + +Good: Define interfaces of all outer-level components. Does your architecture publish a Web API? If not, extend it so that it does. + +Exceed: Also, document the Web API using the OpenAPI language. You can use the [OpenAPI-to-Tree](http://api-ace.inf.usi.ch/openapi-to-tree/) tool to visualize the structure of your OpenAPI description. + +} + +# Ex - Connector View + +{.instructions + +Extend your existing models introducing the connector view + +For every pair of connected components (logical view), pick the most suitable connector. Existing components can play the role of connector, or new connectors may need to be introduced. + +![Example Connector View Diagram](./examples/connector-view.c5) + +Make sure that the interactions shown in the process views reflect the primitives of the selected connector + +Pass: model existing connectors based on previous model decisions + +Good: model existing connectors based on previous model decisions, write an ADR about the choice of one connector + +Exceed: introduce a new type of connector and update your existing process view +(sequence diagram) to show the connector primitives in action + +} + +# Ex - Adapters and Coupling + +{.instructions + +1. Highlight the connectors (or components) in your existing bottom-up design playing the role of adapter. (We suggest to use the bottom-up design since when dealing with externally sourced components, their interfaces can be a source of mismatches). +2. Which kind of mismatch** are they solving? +3. Introduce a wrapper in your architecture to hide one of the previously highlighted adapters +4. Where would standard interfaces play a role in your architecture? Which standards could be relevant in your domain? +5. Explain how one or more pairs of components are coupled according to different coupling facets +6. Provide more details on how each adapter solves the mismatches identified using pseudo-code or the actual code +7. How can you improve your architectural model to minimize coupling between components? (Include a revised logical/connector view with your solution) + +Pass: 1-5 (with one adapter) + +Good: 1-6 (with at least two adapters) + +Exceed: 1-7 (with at least two adapters) + +** If you do not find any mismatch in your existing design we suggest to introduce one artificially. + +## Hints + +* (1) Should we find cases where two components cannot communicate (and are doing it wrongly) and highlight they would need an adapter?, or cases where we have already a "component playing the role of adapter in the view" and highlight only the adapter? + + *Both are fine. We assumed that if you draw a dependency (or a connector) the interfaces match, but if you detect that the components that should communicate cannot communicate then of course introduce an adapter to solve the mismatch* + +* (2) Please show the details about the two interfaces which do not match (e.g., names of parameters, object structures) so that it becomes clear why an adapter is needed and what the adapter should do to bridge the mismatch + +* (5-6) These questions are about the implications on coupling based on the decisions you documented in the connector view. +Whenever you have a connector you couple together the components and different connectors will have different forms of coupling + + For example, if you use calls everywhere, do you really need them everywhere? is there some pair of components where you could use a message queue instead? + + Regarding the coupling facets mentioned in question 5. You do not have to answer all questions related to "discovery", "session", "binding", "interaction", "timing", "interface" and "platform" (p.441, Coupling Facets). Just the ones that you think are relevant for your design and by answering them you can get ideas on how to do question 6. + +} + + + + +# Ex - Physical and Deployment Views + +{.instructions + +a. Extend your architectural model with the following viewpoints: + +1. Physical or Container View + +2. Deployment View + +Your model should be non-trivial: include more than one physical device/virtual container (or both). Be ready to discuss which connectors are found at the device/container boundaries. + +b. Write an ADR about which deployment strategy you plan to adopt. The alternatives to be considered are: big bang, blue/green, shadow, pilot, gradual phase-in, canary, A/B testing. + +c. (Optional) Prepare a demo of a basic continuous integration and delivery pipeline for your architectural documentation so that you can obtain a single, integrated PDF with all the viewpoints you have modeled so far. + +For example: + +- configure a GitHub webhook to be called whenever you push changes to your documentation +- setup a GitHub action (or similar) to build and publish your documentation on a website + +Pass: 1 physical view, 1 deployment view, 1 ADR (b.) + +Good: >1 physical view, >1 deployment view, 1 ADR (b.) + +Exceed: 1 physical view, 1 deployment view, 1 ADR (b.) + 1 demo (c.) + +} + +# Ex - Availability and Services + +{.instructions + +The goal of this week is to plan how to deliver your software as a service with high availability. + +1. If necessary, change your deployment design so that your software is hosted on a server (which could be running as a Cloud VM). Your SaaS architecture should show how your SaaS can be remotely accessed from a client such as a Web browser, or a mobile app +2. Sketch your software as a service pricing model (optional) +3. How would you define the availability requirements in your project domain? For example, what would be your expectation for the duration of planned/unplanned downtimes or the longest response time tolerated by your clients? +4. Which strategy do you adopt to monitor your service's availability? Extend your architecture with a watchdog or a heartbeat monitor and motivate your choice with an ADR. +5. What happens when a stateless component goes down? model a sequence diagram to show what needs to happen to recover one of your critical stateless components +6. How do you plan to recover stateful components? write an ADR about your choice of replication strategy and whether you prefer consistency vs. availability. Also, consider whether event sourcing would help in your context. +7. How do you plan to avoid cascading failures? Be ready to discuss how the connectors (modeled in your connector view) impact the reliability of your architecture. +8. How did you mitigate the impact of your external dependencies being not available? (if applicable) + +Pass: 1, 3, 4, one of: 5, 6, 7, 8 + +Good: 1, 2, 3, 4, two of: 5, 6, 7, 8 + +Exceed: 1, 2, 3, 4, 5, 6, 7, 8 + +} + +# Ex - Flexibility + +{.instructions + +Only dead software stops changing. You just received a message from your customer, they have an idea. Is your architecture ready for it? + +1. Pick a new use case scenario. Precisely, what exactly do you need to change of your existing architecture so that it can be supported? Model the updated logical/process/deployment views. + +2. Pick another use case scenario so that it can be supported without any major architectural change (i.e., while you cannot add new components, it is possible to extend the interface of existing ones or introduce new dependencies). Illustrate with a process view, how your previous design can satisfy the new requirement. + +3. Change impact. One of your externally sourced component/Web service API has announced it will introduce a breaking change. What is the impact of such change? How can you control and limit the impact of such change? Update your logical view + +4. Open up your architecture so that it can be extended with plugins by its end-users. Where would be a good extension point? Update your logical view and give at least one example of what a plugin would actually do. + +5. Assuming you have a centralized deployment with all stateful components storing their state in the same database, propose a strategy to split the monolith into at least two different microservices. Model the new logical/deployment view as well as the interfaces of each microservice you introduce. + +Pass: 1, one out of 2-5. + +Good: 1, two out of 2-5. + +Exceed: 1-5. + +} \ No newline at end of file diff --git a/template/doc.ejs b/template/doc.ejs new file mode 100644 index 0000000..a6b2d44 --- /dev/null +++ b/template/doc.ejs @@ -0,0 +1,248 @@ + + + + <%=meta.author%> - <%=meta.title%> + + + + + + +
+
+

<%=meta.lecture%>

+

<%=meta.title%>

+
+
    +
  • <%=meta.author%>
  • +
+
+ + + +
+ <%-md %> +
+ + + + + + +