├── .gitignore ├── LICENSE ├── README.md ├── ap0B ├── README.md ├── example-frontend │ ├── index.html │ ├── index.jsx │ └── styling.css ├── fibonacci.js ├── stateful-frontend │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ └── styling.css └── test │ └── fibonacci-tests.js ├── ch03 ├── README.md ├── common │ └── ast.js ├── exercises-section3.2.js ├── exercises-section3.3.js ├── listing3.2.js ├── listing3.3.js ├── listing3.4.js ├── listing3.5.js ├── listings3.5-6.js ├── listings3.6-7.js ├── print-pretty.js ├── rental-AST.js ├── section3.3.js ├── snippets-section3.1.js └── snippets-section3.2.js ├── ch04 ├── 4.1-initial │ ├── index.html │ ├── index.jsx │ └── styling.css ├── 4.1-secondary │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ └── styling.css ├── 4.2.1-noKeyAndDiv │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ └── styling.css ├── 4.2.1-withKeyAndDiv │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ └── styling.css ├── 4.2.2-afterFixes │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ └── styling.css ├── 4.2.2-beforeFixes │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ └── styling.css ├── 4.2.3 │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ └── styling.css ├── 4.2 │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ └── styling.css ├── README.md └── common │ └── ast.js ├── ch05 ├── 5.1.0-text-component │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ └── value-components.jsx ├── 5.1.1-make-clickable │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ └── value-components.jsx ├── 5.1.2-introduce-edit-state │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ └── value-components.jsx ├── 5.1.4-observable │ └── mobx.js ├── 5.1.4-react-to-edit-state │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ └── value-components.jsx ├── 5.1.5-stop-editing │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ └── value-components.jsx ├── 5.1.6-update-value │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ └── value-components.jsx ├── 5.2-1-number-component │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ └── value-components.jsx ├── 5.2-2-higher-order-component │ ├── index.html │ ├── index.jsx │ ├── number-value.jsx │ ├── projection.jsx │ ├── styling.css │ ├── text-value.jsx │ └── value-components.jsx ├── 5.2-3-edit-state-factory │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ └── value-components.jsx ├── 5.3-drop-down-component │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ └── value-components.jsx ├── 5.4-choose-reference │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ └── value-components.jsx ├── README.md └── common │ └── ast.js ├── ch06 ├── 6.1.1-1-call-to-action │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ └── value-components.jsx ├── 6.1.1-2-placeholders │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ └── value-components.jsx ├── 6.1.1-3-Refactoring │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── placeholder-dropDownValue.jsx │ ├── placeholder-inputValue.jsx │ ├── projection.jsx │ ├── styling.css │ └── value-components.jsx ├── 6.1.2-1-call-to-action │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── 6.1.2-2-drop-down │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── 6.1.2-3-action-text-in-drop-down │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── 6.1.2-4-fix-unset-attribute-reference │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── 6.2.1-selecting-an-attribute │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── 6.2.2-deselection │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── 6.2.3-1-wrapper-component │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── 6.2.3-2-all-selectable │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── 6.3.1-1-catch-keys │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── 6.3.1-2-delete-initial-values │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── 6.3.2-delete-attributes │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── README.md └── common │ └── ast.js ├── ch07 ├── README.md ├── backend │ ├── .gitignore │ ├── data │ │ └── .gitignore │ ├── deserialize-contents.js │ ├── server.js │ ├── storage.js │ └── test-POST-not-implemented.sh ├── common │ ├── ast.js │ └── file-utils.js ├── frontend │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── init │ ├── example-AST.js │ ├── example-contents.json │ ├── install-example-DSL-content.js │ ├── jsonify-Rental.js │ └── put-example.sh ├── initialize-storage.sh ├── run-Domain-IDE.sh └── test │ └── test-serialization.js ├── ch08 ├── README.md ├── backend │ ├── .gitignore │ ├── data │ │ └── .gitignore │ ├── server.js │ └── storage.js ├── common │ ├── ast.js │ └── file-utils.js ├── frontend │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── generate-and-run-Runtime.sh ├── generator │ ├── generate-retrieve-ast-from-backend.js │ ├── generate.js │ ├── indexJsx-template-01-verbatim-generation-target.js │ ├── indexJsx-template-02-partial-parametrization-on-record-type-name.js │ ├── indexJsx-template-03-camelCasing-record-type-name.js │ ├── indexJsx-template-04-partial-handling-of-attributes-with-problems.js │ ├── indexJsx-template-05-partial-handling-of-attributes-with-indentation-and-newlines.js │ ├── indexJsx-template-06-almost-complete-handling-of-attributes-with-initial-values.js │ ├── indexJsx-template-07-complete-handling-of-attributes-with-initial-values.js │ ├── indexJsx-template-08-use-nested-strings.js │ ├── indexJsx-template-09-declarative-indentation.js │ ├── indexJsx-template.js │ └── template-utils.js ├── init │ ├── example-AST.js │ └── install-example-DSL-content.js ├── initialize-storage.sh ├── run-Domain-IDE.sh └── runtime │ ├── components.jsx │ ├── dates.js │ ├── index.html │ ├── index.jsx │ └── styling.css ├── ch09 ├── README.md ├── backend │ ├── data │ │ └── .gitignore │ ├── server.js │ └── storage.js ├── common │ ├── ast.js │ ├── dependency-utils.js │ └── file-utils.js ├── frontend │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── generate-and-run-Runtime.sh ├── generator │ ├── generate.js │ ├── indexJsx-template.js │ └── template-utils.js ├── init │ ├── example-AST.js │ └── install-example-DSL-content.js ├── initialize-storage.sh ├── language │ ├── constraints-before-DRY.js │ ├── constraints.js │ └── queries.js ├── run-Domain-IDE.sh └── runtime │ ├── components.jsx │ ├── dates.js │ ├── index.html │ ├── index.jsx │ └── styling.css ├── ch10 ├── README.md ├── backend │ ├── data │ │ └── .gitignore │ ├── server.js │ └── storage.js ├── common │ ├── ast.js │ ├── dependency-utils.js │ └── file-utils.js ├── exercise-10.3.md ├── frontend │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── generate-and-run-Runtime.sh ├── generator │ ├── generate.js │ ├── indexJsx-template.js │ └── template-utils.js ├── init │ ├── example-AST.js │ ├── install-example-DSL-content.js │ └── migrations.js ├── initialize-storage.sh ├── language │ ├── constraints.js │ └── queries.js ├── run-Domain-IDE.sh └── runtime │ ├── components.jsx │ ├── dates.js │ ├── index.html │ ├── index.jsx │ └── styling.css ├── ch11 ├── README.md ├── backend │ ├── data │ │ └── .gitignore │ ├── server.js │ └── storage.js ├── common │ ├── ast.js │ ├── dependency-utils.js │ └── file-utils.js ├── frontend │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── generate-and-run-Runtime.sh ├── generator │ ├── generate.js │ ├── indexJsx-template.js │ └── template-utils.js ├── init │ ├── example-AST.js │ ├── expressions-AST.js │ ├── install-example-DSL-content.js │ └── migrations.js ├── initialize-storage.sh ├── language │ ├── constraints.js │ ├── factories.js │ └── queries.js ├── run-Domain-IDE.sh └── runtime │ ├── components.jsx │ ├── dates.js │ ├── index.html │ ├── index.jsx │ └── styling.css ├── ch12 ├── README.md ├── backend │ ├── data │ │ └── .gitignore │ ├── server.js │ └── storage.js ├── common │ ├── ast.js │ ├── dependency-utils.js │ └── file-utils.js ├── frontend │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── generate-and-run-Runtime.sh ├── generator │ ├── generate.js │ ├── indexJsx-template.js │ └── template-utils.js ├── init │ ├── example-AST.js │ ├── install-example-DSL-content.js │ └── migrations.js ├── initialize-storage.sh ├── language │ ├── constraints.js │ ├── factories.js │ ├── operators.js │ └── queries.js ├── run-Domain-IDE.sh └── runtime │ ├── components.jsx │ ├── dates.js │ ├── index.html │ ├── index.jsx │ └── styling.css ├── ch13 ├── README.md ├── backend │ ├── data │ │ └── .gitignore │ ├── server.js │ └── storage.js ├── common │ ├── ast.js │ ├── dependency-utils.js │ └── file-utils.js ├── detailed-code-organization.adoc ├── frontend │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection-after-13.1.js │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── generate-and-run-Runtime.sh ├── generator │ ├── generate.js │ ├── indexJsx-template.js │ └── template-utils.js ├── init │ ├── example-AST.js │ ├── install-example-DSL-content.js │ └── migrations.js ├── initialize-storage.sh ├── language │ ├── constraints-after-13.2.js │ ├── constraints.js │ ├── factories.js │ ├── operators.js │ ├── queries.js │ ├── specification-type-system.adoc │ ├── type-system-after-13.1.js │ └── type-system.js ├── run-Domain-IDE.sh ├── runtime │ ├── components.jsx │ ├── dates.js │ ├── index.html │ ├── index.jsx │ └── styling.css └── test │ └── test-type-system.js ├── ch14 ├── README.md ├── backend │ ├── data │ │ └── .gitignore │ ├── server.js │ └── storage.js ├── common │ ├── ast.js │ ├── dependency-utils.js │ └── file-utils.js ├── detailed-code-organization.adoc ├── frontend │ ├── css-util.js │ ├── index.html │ ├── index.jsx │ ├── projection.jsx │ ├── styling.css │ ├── support-components.jsx │ └── value-components.jsx ├── generate-and-run-Runtime.sh ├── generator │ ├── generate.js │ ├── indexJsx-template.js │ └── template-utils.js ├── init │ ├── example-AST.js │ ├── install-example-DSL-content.js │ └── migrations.js ├── initialize-storage.sh ├── language │ ├── constraints.js │ ├── factories.js │ ├── operators.js │ ├── queries.js │ ├── time-units.js │ └── type-system.js ├── run-Domain-IDE.sh └── runtime │ ├── components.jsx │ ├── dates.js │ ├── index.html │ ├── index.jsx │ └── styling.css ├── ch15 ├── README.md ├── meta-model │ └── concepts.js ├── metamodel │ └── concepts.js └── textual │ ├── Rental.txt │ └── recordType.g4 ├── jsconfig.json ├── mps ├── .gitattributes ├── .gitignore ├── .mps │ ├── .name │ ├── migration.xml │ ├── modules.xml │ └── vcs.xml ├── languages │ └── BusinessDsl │ │ ├── BusinessDsl.mpl │ │ ├── generator │ │ └── templates │ │ │ └── BusinessDsl.generator.templates@generator.mps │ │ └── models │ │ ├── BusinessDsl.behavior.mps │ │ ├── BusinessDsl.constraints.mps │ │ ├── BusinessDsl.editor.mps │ │ ├── BusinessDsl.intentions.mps │ │ ├── BusinessDsl.structure.mps │ │ └── BusinessDsl.typesystem.mps └── solutions │ └── CarRentalCompany │ ├── CarRentalCompany.msd │ └── models │ └── DSLContent.mps ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # generated by NPM: 2 | /npm-error.log 3 | 4 | # generated by Yarn: 5 | /yarn.lock 6 | /yarn-error.log 7 | 8 | # downloaded dependencies: 9 | /node_modules/ 10 | 11 | # generated by Parcel.js: 12 | /.parcel-cache/ 13 | dist/ 14 | # special version for Runtime, to deconflict from Parcel's default: 15 | dist-runtime/ 16 | 17 | # for JetBrains' IntelliJ/IDEA: 18 | /.idea/ 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Meinte Boersma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building-User Friendly-DSLs-code 2 | 3 | All the code from the book [“Building User-Friendly DSLs”](https://www.manning.com/books/building-user-friendly-dsls) ([liveBook](https://livebook.manning.com/book/building-user-friendly-dsls/)) by Meinte Boersma for Manning Publications. 4 | 5 | 6 | ## Installation instructions 7 | 8 | Running the code requires a recent version of [Node.js](https://nodejs.org/en/) and an NPM-compatible package manager, so either NPM itself or [Yarn](https://yarnpkg.com/). 9 | For more details: see appendix A of the book. 10 | 11 | 12 | ## Running the code 13 | 14 | This repository contains the code developed in the course of chapters 3-15 and appendix B of the book, with one directory per chapter/appendix. 15 | (Chapter 2 contains a static mockup of DSL content.) 16 | Each directory contains a README detailing how to run the code. 17 | 18 | 19 | ## Notes 20 | 21 | * Please report issues preferably through the [liveBook](https://livebook.manning.com/book/building-user-friendly-dsls/), or otherwise through the [GitHub issues page](https://github.com/dslmeinte/Building-User-Friendly-DSLs-code/issues). 22 | * Because the code is taken from the book, there's virtually no possibility to accept feature requests or Pull Requests. 23 | * `package.json`/`version` is of the form `0.n.0-dev`, with `n` = currently-published MEAP-version + 1. 24 | 25 | 26 | ## Copyright 27 | 28 | Copyright retained by Meinte Boersma for Manning Publications. 29 | 30 | -------------------------------------------------------------------------------- /ap0B/README.md: -------------------------------------------------------------------------------- 1 | # Code from appendix B 2 | 3 | * [An example frontend](./example-frontend) that's not TFRP. 4 | 5 | * [A _stateful_ example frontend](./stateful-frontend) using TFRP through MobX. 6 | 7 | * For unit testing (see § B.1.11): 8 | * [An implementation of the Fibonacci function](./fibonacci.js). 9 | * [Unit tests for that](./test/fibonacci-tests.js). 10 | 11 | -------------------------------------------------------------------------------- /ap0B/example-frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ap0B/example-frontend/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | createRoot(document.getElementById("root")) 7 | .render( 8 | Frontend goes here! 9 | ) 10 | 11 | 12 | require("../../../src/frontend/grayscale") 13 | 14 | -------------------------------------------------------------------------------- /ap0B/example-frontend/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | -------------------------------------------------------------------------------- /ap0B/fibonacci.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Inefficient calculation of Fibonacci numbers using recursion into a function that's defined with a `const` declaration. 3 | * This proves that we don't need to define functions using the `function` keyword 4 | * - not even if they're recursive, and have to run under Node.js. 5 | */ 6 | const fibonacci = (n) => n <= 1 ? n : fibonacci(n - 2) + fibonacci(n - 1) 7 | module.exports.fibonacci = fibonacci 8 | 9 | -------------------------------------------------------------------------------- /ap0B/stateful-frontend/css-util.js: -------------------------------------------------------------------------------- 1 | const asClassNameArgument = (...classNames) => 2 | classNames.filter((className) => typeof className === "string").join(" ") 3 | module.exports.asClassNameArgument = asClassNameArgument 4 | 5 | -------------------------------------------------------------------------------- /ap0B/stateful-frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ap0B/stateful-frontend/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { action, observable } from "mobx" 4 | import { observer } from "mobx-react" 5 | 6 | 7 | const state = observable({ 8 | counter: 0 9 | }) 10 | 11 | const CounterComponent = observer(({ state }) => 12 | 15 | ) 16 | 17 | createRoot(document.getElementById("root")) 18 | .render( 19 | 20 | ) 21 | 22 | 23 | require("../../../src/frontend/grayscale") 24 | 25 | -------------------------------------------------------------------------------- /ap0B/stateful-frontend/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | -------------------------------------------------------------------------------- /ap0B/test/fibonacci-tests.js: -------------------------------------------------------------------------------- 1 | const { equal } = require("chai").assert 2 | 3 | const { fibonacci } = require("../fibonacci") 4 | 5 | describe("recursive functions defined as a `const` declaration", (_) => { 6 | 7 | it("Fibonacci works", (done) => { 8 | equal(fibonacci(0), 0) 9 | equal(fibonacci(1), 1) 10 | equal(fibonacci(2), 1) 11 | equal(fibonacci(3), 2) 12 | equal(fibonacci(4), 3) 13 | equal(fibonacci(5), 5) 14 | equal(fibonacci(6), 8) 15 | equal(fibonacci(7), 13) 16 | equal(fibonacci(8), 21) 17 | equal(fibonacci(9), 34) 18 | equal(fibonacci(10), 55) 19 | done() 20 | }) 21 | 22 | }) 23 | 24 | -------------------------------------------------------------------------------- /ch03/README.md: -------------------------------------------------------------------------------- 1 | # Source code chapter 3 2 | 3 | This directory contains all the source code in chapter arranged/subdivided as follows, in the order it appears in the book's text: 4 | 5 | * [Listing 3.1.: `print-pretty.js`](./print-pretty.js) 6 | * [Source code "snippets" in section § 3.1](snippets-section3.1.js) 7 | * [Listing 3.3.: construction of the “Rental” AST](./rental-AST.js) 8 | * [Source code "snippets" in section § 3.2](snippets-section3.2.js) 9 | * [Listing 3.4.](./listing3.4.js) 10 | * [Listing 3.5.](./listing3.5.js) 11 | * [Listings 3.6. and 3.7.](./listings3.6-7.js) 12 | * [Listing 3.8.: `ast.js`](./common/ast.js) 13 | * [Reference solution to Exercise 3.1 of section § 3.2](./exercises-section3.2.js) 14 | * [Code from section § 3.3](./section3.3.js) 15 | * [Reference solutions to Exercises 3.2, 3.4-3.6 of section § 3.3](./exercises-section3.3.js) 16 | 17 | -------------------------------------------------------------------------------- /ch03/common/ast.js: -------------------------------------------------------------------------------- 1 | // Listing 3.6: 2 | 3 | const isObject = (value) => (!!value) && (typeof value === "object") && !Array.isArray(value) 4 | 5 | const isAstObject = (value) => isObject(value) && ("concept" in value) && ("settings" in value) 6 | module.exports.isAstObject = isAstObject 7 | 8 | 9 | const isAstReferenceObject = (value) => isObject(value) && ("ref" in value) 10 | 11 | const isAstReference = (value) => isAstReferenceObject(value) && isAstObject(value.ref) 12 | module.exports.isAstReference = isAstReference 13 | 14 | -------------------------------------------------------------------------------- /ch03/exercises-section3.2.js: -------------------------------------------------------------------------------- 1 | // Verbatim-copy of function in './ast.js' since it's undesirable to export that from there: 2 | const isObject = (value) => (!!value) && (typeof value === "object") && !Array.isArray(value) 3 | 4 | // AST to test with: 5 | const rental = require("./rental-AST") 6 | 7 | 8 | // Exercise 3.1: 9 | 10 | const isAstObject = (value) => isObject(value) && ("concept" in value && typeof value.concept === "string") && ("settings" in value && isObject(value.settings)) 11 | // ^^^^ check that concept is a string ^^^^ check that settings is an object 12 | 13 | console.log(isAstObject({ concept: 1, settings: {} })) // false 14 | console.log(isAstObject({ concept: "Foo", settings: [] })) // false 15 | console.log(isAstObject(rental)) // true (check that adapted isAstObject is not too strict) 16 | 17 | -------------------------------------------------------------------------------- /ch03/listing3.3.js: -------------------------------------------------------------------------------- 1 | // Listing 3.3: 2 | 3 | const isObject = (value) => (!!value) && (typeof value === "object") && !Array.isArray(value) 4 | 5 | // Export statement to be able to import this function in 'snippets-section3.2.js': 6 | module.exports = isObject 7 | 8 | -------------------------------------------------------------------------------- /ch03/listing3.4.js: -------------------------------------------------------------------------------- 1 | // Listing 3.4: 2 | 3 | const isObject = (value) => (!!value) && (typeof value === "object") && !Array.isArray(value) 4 | 5 | // Export statement to be able to import this function in 'snippets-section3.2.js': 6 | module.exports = isObject 7 | 8 | -------------------------------------------------------------------------------- /ch03/listing3.5.js: -------------------------------------------------------------------------------- 1 | // Listing 3.5: 2 | 3 | const isAstObject = (value) => isObject(value) && ("concept" in value) && ("settings" in value) 4 | 5 | // Export statement to be able to import this function in 'snippets-section3.2.js': 6 | module.exports = isAstObject 7 | 8 | -------------------------------------------------------------------------------- /ch03/listings3.5-6.js: -------------------------------------------------------------------------------- 1 | // Listing 3.5: 2 | 3 | const isAstReferenceObject = (value) => isObject(value) && ("ref" in value) 4 | 5 | 6 | // Listing 3.6: 7 | 8 | const isAstReference = (value) => isAstReferenceObject(value) && isAstObject(value.ref) 9 | 10 | // Export statement to be able to import this function in 'snippets-section3.2.js': 11 | module.exports = isAstReference 12 | 13 | -------------------------------------------------------------------------------- /ch03/listings3.6-7.js: -------------------------------------------------------------------------------- 1 | // Listing 3.6: 2 | 3 | const isAstReferenceObject = (value) => isObject(value) && ("ref" in value) 4 | 5 | 6 | // Listing 3.7: 7 | 8 | const isAstReference = (value) => isAstReferenceObject(value) && isAstObject(value.ref) 9 | 10 | // Export statement to be able to import this function in 'snippets-section3.2.js': 11 | module.exports = isAstReference 12 | 13 | -------------------------------------------------------------------------------- /ch03/print-pretty.js: -------------------------------------------------------------------------------- 1 | // Listing 3.1: 2 | 3 | module.exports = (value) => { 4 | console.log(JSON.stringify(value, null, 2)) 5 | } 6 | 7 | -------------------------------------------------------------------------------- /ch03/section3.3.js: -------------------------------------------------------------------------------- 1 | const { isAstObject, isAstReference } = require("./common/ast") 2 | 3 | 4 | const numberOfLeaves = (value) => { 5 | 6 | // Exercise 3.3: 7 | console.log(`numberOfLeaves called with following value:`) 8 | console.dir(value) 9 | console.log() // newline, for separation 10 | 11 | if (isAstObject(value)) { 12 | const sub = sum(Object.values(value.settings).map(numberOfLeaves)) 13 | return sub === 0 ? 1 : sub 14 | } 15 | if (isAstReference(value)) { 16 | return 0 17 | } 18 | if (Array.isArray(value)) { 19 | return sum(value.map(numberOfLeaves)) 20 | } 21 | return 0 22 | } 23 | 24 | 25 | const sum = (numbers) => 26 | numbers.reduce((currentSum, currentNumber) => currentSum + currentNumber, 0) 27 | module.exports.sum = sum // We'll reuse this function in exercises-section3.3.js. 28 | 29 | 30 | const rental = require("./rental-AST") 31 | console.log(`numberOfLeaves(“Rental“ AST)=${numberOfLeaves(rental)}`) 32 | console.log() 33 | 34 | -------------------------------------------------------------------------------- /ch04/4.1-initial/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch04/4.1-initial/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | createRoot(document.getElementById("root")) 7 | .render( 8 | Frontend goes here! 9 | ) 10 | 11 | -------------------------------------------------------------------------------- /ch04/4.1-initial/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /ch04/4.1-secondary/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch04/4.1-secondary/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | import rental from "../../ch03/rental-AST" 7 | 8 | import { Projection } from "./projection" 9 | 10 | createRoot(document.getElementById("root")) 11 | .render( 12 | 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /ch04/4.1-secondary/projection.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | 4 | export const Projection = ({ astObject }) => Projection should go here! 5 | 6 | -------------------------------------------------------------------------------- /ch04/4.1-secondary/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /ch04/4.2.1-noKeyAndDiv/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch04/4.2.1-noKeyAndDiv/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | import rental from "../../ch03/rental-AST" 7 | 8 | import { Projection } from "./projection" 9 | 10 | createRoot(document.getElementById("root")) 11 | .render( 12 | 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /ch04/4.2.1-noKeyAndDiv/projection.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import { isAstObject } from "../common/ast" 4 | 5 | 6 | export const Projection = ({ astObject }) => { 7 | if (isAstObject(astObject)) { 8 | switch (astObject.concept) { 9 | 10 | case "Record Type": return
11 |
12 | Record Type 13 | {astObject.settings["name"]} 14 |
15 |
16 |
attributes:
17 | {astObject.settings["attributes"].map((attribute) => )} 18 |
19 |
20 | 21 | default: return {"No projection defined for concept: " + astObject.concept} 22 | } 23 | } 24 | return {"No projection defined for value: " + astObject} 25 | } 26 | 27 | -------------------------------------------------------------------------------- /ch04/4.2.1-noKeyAndDiv/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ch04/4.2.1-withKeyAndDiv/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch04/4.2.1-withKeyAndDiv/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | import rental from "../../ch03/rental-AST" 7 | 8 | import { Projection } from "./projection" 9 | 10 | createRoot(document.getElementById("root")) 11 | .render( 12 | 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /ch04/4.2.1-withKeyAndDiv/projection.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import { isAstObject } from "../common/ast" 4 | 5 | 6 | export const Projection = ({ astObject }) => { 7 | if (isAstObject(astObject)) { 8 | switch (astObject.concept) { 9 | 10 | case "Record Type": return
11 |
12 | Record Type 13 | {astObject.settings["name"]} 14 |
15 |
16 |
attributes:
17 | {astObject.settings["attributes"].map((attribute, index) => 18 | 19 | )} 20 |
21 |
22 | 23 | default: return
24 | {"No projection defined for concept: " + astObject.concept} 25 |
26 | } 27 | } 28 | 29 | return {"No projection defined for value: " + astObject} 30 | } 31 | 32 | -------------------------------------------------------------------------------- /ch04/4.2.1-withKeyAndDiv/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ch04/4.2.2-afterFixes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch04/4.2.2-afterFixes/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | import rental from "../../ch03/rental-AST" 7 | 8 | import { Projection } from "./projection" 9 | 10 | createRoot(document.getElementById("root")) 11 | .render( 12 | 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /ch04/4.2.2-afterFixes/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | div.inline { 41 | display: inline-block; 42 | } 43 | 44 | span.enum-like { 45 | font-style: italic; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /ch04/4.2.2-beforeFixes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch04/4.2.2-beforeFixes/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | import rental from "../../ch03/rental-AST" 7 | 8 | import { Projection } from "./projection" 9 | 10 | createRoot(document.getElementById("root")) 11 | .render( 12 | 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /ch04/4.2.2-beforeFixes/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /ch04/4.2.3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch04/4.2.3/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | import rental from "../../ch03/rental-AST" 7 | 8 | import { Projection } from "./projection" 9 | 10 | createRoot(document.getElementById("root")) 11 | .render( 12 | 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /ch04/4.2.3/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | div.inline { 45 | display: inline-block; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /ch04/4.2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch04/4.2/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | import rental from "../../ch03/rental-AST" 7 | 8 | import { Projection } from "./projection" 9 | 10 | createRoot(document.getElementById("root")) 11 | .render( 12 | 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /ch04/4.2/projection.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import { isAstObject } from "../common/ast" 4 | 5 | 6 | export const Projection = ({ astObject }) => { 7 | if (isAstObject(astObject)) { 8 | switch (astObject.concept) { 9 | default: return {"No projection defined for concept: " + astObject.concept} 10 | } 11 | } 12 | return {"No projection defined for value: " + astObject} 13 | } 14 | 15 | -------------------------------------------------------------------------------- /ch04/4.2/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /ch04/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | The DSL-*aspecific* code used by the frontends - currently only `ast.js` - is located in the `common/` directory. 4 | Each of the other directories contains a version of the frontend. 5 | The names indicate a section (§ .) of the chapter, usually (but optionally) followed by a hyphen and a further indication of the intention of that version. 6 | That indication can itself be numbered again, so the entire format for the directory names is: `.[-[-]]` 7 | 8 | Each of the frontend versions can be run by running the following commandline command 9 | 10 | ```shell 11 | $ npx parcel /index.html 12 | ``` 13 | 14 | and opening [`http://localhost:1234/`](http://localhost:1234/) in a browser. 15 | 16 | -------------------------------------------------------------------------------- /ch04/common/ast.js: -------------------------------------------------------------------------------- 1 | const isObject = (value) => (!!value) && (typeof value === "object") && !Array.isArray(value) 2 | 3 | const isAstObject = (value) => isObject(value) && ("concept" in value) && ("settings" in value) 4 | module.exports.isAstObject = isAstObject 5 | 6 | 7 | const isAstReferenceObject = (value) => isObject(value) && ("ref" in value) 8 | 9 | const isAstReference = (value) => isAstReferenceObject(value) && isAstObject(value.ref) 10 | module.exports.isAstReference = isAstReference 11 | 12 | -------------------------------------------------------------------------------- /ch05/5.1.0-text-component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch05/5.1.0-text-component/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | import rental from "../../ch03/rental-AST" 7 | 8 | import { Projection } from "./projection" 9 | 10 | createRoot(document.getElementById("root")) 11 | .render( 12 | 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /ch05/5.1.0-text-component/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | div.inline { 45 | display: inline-block; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /ch05/5.1.0-text-component/value-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | 4 | export const TextValue = ({ value }) => {value} 5 | 6 | -------------------------------------------------------------------------------- /ch05/5.1.1-make-clickable/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch05/5.1.1-make-clickable/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | import rental from "../../ch03/rental-AST" 7 | 8 | import { Projection } from "./projection" 9 | 10 | createRoot(document.getElementById("root")) 11 | .render( 12 | 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /ch05/5.1.1-make-clickable/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | div.inline { 45 | display: inline-block; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /ch05/5.1.1-make-clickable/value-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | 4 | export const TextValue = ({ value }) => 5 | { 7 | alert(`Editing of text value "${value}" started!`) 8 | }} 9 | >{value} 10 | 11 | -------------------------------------------------------------------------------- /ch05/5.1.2-introduce-edit-state/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch05/5.1.2-introduce-edit-state/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | import rental from "../../ch03/rental-AST" 7 | 8 | import { Projection } from "./projection" 9 | 10 | createRoot(document.getElementById("root")) 11 | .render( 12 | 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /ch05/5.1.2-introduce-edit-state/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | div.inline { 45 | display: inline-block; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /ch05/5.1.2-introduce-edit-state/value-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | 4 | export const TextValue = ({ editState }) => 5 | editState.inEdit 6 | ? 10 | : { 12 | editState.inEdit = true 13 | }} 14 | >{editState.value} 15 | 16 | -------------------------------------------------------------------------------- /ch05/5.1.4-observable/mobx.js: -------------------------------------------------------------------------------- 1 | const { observable, spy } = require("mobx") 2 | 3 | 4 | const someObject = { 5 | "prop1": "value0" 6 | } 7 | 8 | const someObservableObject = observable(someObject) 9 | 10 | 11 | spy((change) => { 12 | console.dir(change) 13 | }) 14 | 15 | 16 | someObservableObject["prop1"] = "value1" // produces a spy'd update 17 | 18 | console.log(someObject["prop1"]) // "value0": changes to someObservableObject don't affect someObject 19 | 20 | someObject["prop2"] = "value2" // doesn't produce a spy'd update 21 | 22 | console.log(someObservableObject["prop2"]) // undefined: changes to someObject don't affect someObservableObject 23 | 24 | -------------------------------------------------------------------------------- /ch05/5.1.4-react-to-edit-state/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch05/5.1.4-react-to-edit-state/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | import rental from "../../ch03/rental-AST" 7 | 8 | import { Projection } from "./projection" 9 | 10 | createRoot(document.getElementById("root")) 11 | .render( 12 | 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /ch05/5.1.4-react-to-edit-state/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | div.inline { 45 | display: inline-block; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | input { 55 | font-size: 12pt; 56 | border-radius: 5px; 57 | border: 3px solid yellow; 58 | background-color: lightyellow; 59 | padding-left: 5px; 60 | } 61 | 62 | input:focus { 63 | outline: none; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /ch05/5.1.4-react-to-edit-state/value-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | // Add imports for MobX: 4 | import { action } from "mobx" 5 | import { observer } from "mobx-react" 6 | 7 | 8 | // Wrap the React component functions with observer(...): 9 | export const TextValue = observer(({ editState }) => 10 | editState.inEdit 11 | ? 15 | : { 18 | editState.inEdit = true 19 | })} 20 | >{editState.value} 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /ch05/5.1.5-stop-editing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch05/5.1.5-stop-editing/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | 4 | require("./styling.css") 5 | 6 | import rental from "../../ch03/rental-AST" 7 | 8 | import { Projection } from "./projection" 9 | 10 | createRoot(document.getElementById("root")) 11 | .render( 12 | 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /ch05/5.1.5-stop-editing/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | div.inline { 45 | display: inline-block; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | input { 55 | font-size: 12pt; 56 | border-radius: 5px; 57 | border: 3px solid yellow; 58 | background-color: lightyellow; 59 | padding-left: 5px; 60 | } 61 | 62 | input:focus { 63 | outline: none; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /ch05/5.1.5-stop-editing/value-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | import { observer } from "mobx-react" 4 | 5 | 6 | export const TextValue = observer(({ editState }) => 7 | editState.inEdit 8 | ? { 13 | editState.inEdit = false 14 | })} 15 | // React to special keys to exit editing: 16 | onKeyUp={action((event) => { 17 | if (event.key === "Enter" || event.key === "Escape") { 18 | editState.inEdit = false 19 | } 20 | })} 21 | /> 22 | : { 24 | editState.inEdit = true 25 | })} 26 | >{editState.value} 27 | ) 28 | 29 | -------------------------------------------------------------------------------- /ch05/5.1.6-update-value/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch05/5.1.6-update-value/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /ch05/5.1.6-update-value/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | div.inline { 45 | display: inline-block; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | input { 55 | font-size: 12pt; 56 | border-radius: 5px; 57 | border: 3px solid yellow; 58 | background-color: lightyellow; 59 | padding-left: 5px; 60 | } 61 | 62 | input:focus { 63 | outline: none; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /ch05/5.1.6-update-value/value-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | import { observer } from "mobx-react" 4 | 5 | 6 | export const TextValue = observer(({ editState }) => 7 | editState.inEdit 8 | ? { 12 | const newValue = event.target.value 13 | editState.setValue(newValue) 14 | editState.inEdit = false 15 | })} 16 | onKeyUp={action((event) => { 17 | if (event.key === "Enter") { 18 | const newValue = event.target.value 19 | editState.setValue(newValue) 20 | editState.inEdit = false 21 | } 22 | if (event.key === "Escape") { 23 | editState.inEdit = false 24 | } 25 | })} 26 | /> 27 | : { 29 | editState.inEdit = true 30 | })} 31 | >{editState.value} 32 | ) 33 | 34 | -------------------------------------------------------------------------------- /ch05/5.2-1-number-component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch05/5.2-1-number-component/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /ch05/5.2-1-number-component/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | div.inline { 45 | display: inline-block; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | input { 55 | font-size: 12pt; 56 | border-radius: 5px; 57 | border: 3px solid yellow; 58 | background-color: lightyellow; 59 | padding-left: 5px; 60 | } 61 | 62 | input:focus { 63 | outline: none; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /ch05/5.2-2-higher-order-component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch05/5.2-2-higher-order-component/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /ch05/5.2-2-higher-order-component/number-value.jsx: -------------------------------------------------------------------------------- 1 | const isNumber = (str) => !isNaN(str) && (str.trim().length > 0) 2 | 3 | export const NumberValue = observer(({ editState }) => 4 | editState.inEdit 5 | ? { 9 | const newValue = event.target.value 10 | if (isNumber(newValue)) { 11 | editState.setValue(newValue) 12 | } 13 | editState.inEdit = false 14 | })} 15 | onKeyUp={action((event) => { 16 | if (event.key === "Enter") { 17 | const newValue = event.target.value 18 | if (isNumber(newValue)) { 19 | editState.setValue(newValue) 20 | editState.inEdit = false 21 | } 22 | } 23 | if (event.key === "Escape") { 24 | editState.inEdit = false 25 | } 26 | })} 27 | /> 28 | : { 30 | editState.inEdit = true 31 | })} 32 | >{editState.value} 33 | ) 34 | -------------------------------------------------------------------------------- /ch05/5.2-2-higher-order-component/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | div.inline { 45 | display: inline-block; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | input { 55 | font-size: 12pt; 56 | border-radius: 5px; 57 | border: 3px solid yellow; 58 | background-color: lightyellow; 59 | padding-left: 5px; 60 | } 61 | 62 | input:focus { 63 | outline: none; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /ch05/5.2-2-higher-order-component/text-value.jsx: -------------------------------------------------------------------------------- 1 | 2 | export const TextValue = observer(({ editState }) => 3 | editState.inEdit 4 | ? { 8 | const newValue = event.target.value 9 | editState.setValue(newValue) 10 | editState.inEdit = false 11 | })} 12 | onKeyUp={action((event) => { 13 | if (event.key === "Enter") { 14 | const newValue = event.target.value 15 | editState.setValue(newValue) 16 | editState.inEdit = false 17 | 18 | } 19 | if (event.key === "Escape") { 20 | editState.inEdit = false 21 | } 22 | })} 23 | /> 24 | : { 26 | editState.inEdit = true 27 | })} 28 | >{editState.value} 29 | ) 30 | -------------------------------------------------------------------------------- /ch05/5.2-3-edit-state-factory/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch05/5.2-3-edit-state-factory/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /ch05/5.2-3-edit-state-factory/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | div.inline { 45 | display: inline-block; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | input { 55 | font-size: 12pt; 56 | border-radius: 5px; 57 | border: 3px solid yellow; 58 | background-color: lightyellow; 59 | padding-left: 5px; 60 | } 61 | 62 | input:focus { 63 | outline: none; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /ch05/5.3-drop-down-component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch05/5.3-drop-down-component/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /ch05/5.3-drop-down-component/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | div.inline { 45 | display: inline-block; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | input { 55 | font-size: 12pt; 56 | border-radius: 5px; 57 | border: 3px solid yellow; 58 | background-color: lightyellow; 59 | padding-left: 5px; 60 | } 61 | 62 | input:focus { 63 | outline: none; 64 | } 65 | 66 | select { 67 | font-size: 12pt; 68 | } 69 | 70 | -------------------------------------------------------------------------------- /ch05/5.4-choose-reference/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch05/5.4-choose-reference/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /ch05/5.4-choose-reference/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | div.inline { 45 | display: inline-block; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | input { 55 | font-size: 12pt; 56 | border-radius: 5px; 57 | border: 3px solid yellow; 58 | background-color: lightyellow; 59 | padding-left: 5px; 60 | } 61 | 62 | input:focus { 63 | outline: none; 64 | } 65 | 66 | select { 67 | font-size: 12pt; 68 | } 69 | 70 | -------------------------------------------------------------------------------- /ch05/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | The DSL-*aspecific* code used by the frontends - currently only `ast.js` - is located in the `common/` directory. 4 | Each of the other directories contains a version of the frontend. 5 | The names indicate a section (§ .) of the chapter, usually (but optionally) followed by a hyphen and a further indication of the intention of that version. 6 | That indication can itself be numbered again, so the entire format for the directory names is: `.[-[-]]` 7 | 8 | Each of the frontend versions can be run by running the following commandline command 9 | 10 | ```shell 11 | $ npx parcel /index.html 12 | ``` 13 | 14 | and opening [`http://localhost:1234/`](http://localhost:1234/) in a browser. 15 | 16 | -------------------------------------------------------------------------------- /ch05/common/ast.js: -------------------------------------------------------------------------------- 1 | const isObject = (value) => (!!value) && (typeof value === "object") && !Array.isArray(value) 2 | 3 | const isAstObject = (value) => isObject(value) && ("concept" in value) && ("settings" in value) 4 | module.exports.isAstObject = isAstObject 5 | 6 | 7 | const isAstReferenceObject = (value) => isObject(value) && ("ref" in value) 8 | 9 | const isAstReference = (value) => isAstReferenceObject(value) && isAstObject(value.ref) 10 | module.exports.isAstReference = isAstReference 11 | 12 | -------------------------------------------------------------------------------- /ch06/6.1.1-1-call-to-action/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.1.1-1-call-to-action/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /ch06/6.1.1-1-call-to-action/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | span.keyword { 7 | font-weight: bolder; 8 | color: rgb(100, 100, 100); 9 | } 10 | 11 | .ws-right { 12 | margin-right: 0.5rem; 13 | } 14 | 15 | .ws-left { 16 | margin-left: 0.5rem; 17 | } 18 | 19 | .ws-both { 20 | margin-right: 0.5rem; 21 | margin-left: 0.5rem; 22 | } 23 | 24 | span.value { 25 | padding-left: 0.15em; 26 | padding-right: 0.15em; 27 | border-radius: 5px; 28 | background-color: rgb(228, 228, 228); 29 | } 30 | 31 | div.section { 32 | margin-top: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | div.attribute { 37 | margin-left: 1em; 38 | } 39 | 40 | span.enum-like { 41 | font-style: italic; 42 | } 43 | 44 | div.inline { 45 | display: inline-block; 46 | } 47 | 48 | span.reference { 49 | font-style: italic; 50 | color: blue; 51 | text-decoration: underline; 52 | } 53 | 54 | input { 55 | font-size: 12pt; 56 | border-radius: 5px; 57 | border: 3px solid yellow; 58 | background-color: lightyellow; 59 | padding-left: 5px; 60 | } 61 | 62 | input:focus { 63 | outline: none; 64 | } 65 | 66 | select { 67 | font-size: 12pt; 68 | } 69 | 70 | button.add-new { 71 | font-size: 18pt; 72 | border-radius: 10px; 73 | border: 0; 74 | background-color: rgb(49, 161, 49); 75 | cursor: pointer; 76 | } 77 | 78 | -------------------------------------------------------------------------------- /ch06/6.1.1-2-placeholders/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.1.1-2-placeholders/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | // pre-add an attribute for quick testing: 10 | rental.settings["attributes"].push({ 11 | concept: "Data Attribute", 12 | settings: {} 13 | }) 14 | 15 | import { Projection } from "./projection" 16 | 17 | createRoot(document.getElementById("root")) 18 | .render( 19 | 23 | ) 24 | 25 | -------------------------------------------------------------------------------- /ch06/6.1.1-3-Refactoring/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch06/6.1.1-3-Refactoring/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.1.1-3-Refactoring/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | // pre-add an attribute for quick testing: 10 | rental.settings["attributes"].push({ 11 | concept: "Data Attribute", 12 | settings: {} 13 | }) 14 | 15 | import { Projection } from "./projection" 16 | 17 | createRoot(document.getElementById("root")) 18 | .render( 19 | 23 | ) 24 | 25 | -------------------------------------------------------------------------------- /ch06/6.1.1-3-Refactoring/placeholder-dropDownValue.jsx: -------------------------------------------------------------------------------- 1 | import {observer} from "mobx-react"; 2 | import {action} from "mobx"; 3 | import React from "react"; 4 | 5 | const isMissing = (value) => value === null || value === undefined 6 | 7 | 8 | 9 | 10 | export const DropDownValue = observer(({ editState, className, options, placeholderText }) => 11 | editState.inEdit 12 | ? 34 | : { 37 | editState.inEdit = true 38 | })} 39 | >{isMissing(editState.value) ? placeholderText : editState.value} 40 | ) 41 | -------------------------------------------------------------------------------- /ch06/6.1.2-1-call-to-action/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch06/6.1.2-1-call-to-action/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.1.2-1-call-to-action/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | // pre-add an attribute for quick testing: 10 | rental.settings["attributes"].push({ 11 | concept: "Data Attribute", 12 | settings: { 13 | "name": "new attribute", 14 | "type": "amount" 15 | } 16 | }) 17 | 18 | import { Projection } from "./projection" 19 | 20 | createRoot(document.getElementById("root")) 21 | .render( 22 | 26 | ) 27 | 28 | -------------------------------------------------------------------------------- /ch06/6.1.2-1-call-to-action/support-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | 4 | 5 | export const AddNewButton = ({ buttonText, actionFunction }) => 6 | 13 | 14 | -------------------------------------------------------------------------------- /ch06/6.1.2-2-drop-down/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch06/6.1.2-2-drop-down/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.1.2-2-drop-down/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | import { placeholderAstObject } from "../common/ast" 9 | 10 | // pre-add an attribute for quick testing: 11 | rental.settings["attributes"].push({ 12 | concept: "Data Attribute", 13 | settings: { 14 | "name": "new attribute", 15 | "type": "amount", 16 | // emulate "+ initial value" already being clicked: 17 | "initial value": placeholderAstObject 18 | } 19 | }) 20 | 21 | import { Projection } from "./projection" 22 | 23 | createRoot(document.getElementById("root")) 24 | .render( 25 | 29 | ) 30 | 31 | -------------------------------------------------------------------------------- /ch06/6.1.2-2-drop-down/support-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | 4 | 5 | export const AddNewButton = ({ buttonText, actionFunction }) => 6 | 13 | 14 | -------------------------------------------------------------------------------- /ch06/6.1.2-3-action-text-in-drop-down/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch06/6.1.2-3-action-text-in-drop-down/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.1.2-3-action-text-in-drop-down/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | import { placeholderAstObject } from "../common/ast" 9 | 10 | // pre-add an attribute for quick testing: 11 | rental.settings["attributes"].push({ 12 | concept: "Data Attribute", 13 | settings: { 14 | "name": "new attribute", 15 | "type": "amount", 16 | // emulate "+ initial value" already being clicked: 17 | "initial value": placeholderAstObject 18 | } 19 | }) 20 | 21 | import { Projection } from "./projection" 22 | 23 | createRoot(document.getElementById("root")) 24 | .render( 25 | 29 | ) 30 | 31 | -------------------------------------------------------------------------------- /ch06/6.1.2-3-action-text-in-drop-down/support-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | 4 | 5 | export const AddNewButton = ({ buttonText, actionFunction }) => 6 | 13 | 14 | -------------------------------------------------------------------------------- /ch06/6.1.2-4-fix-unset-attribute-reference/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch06/6.1.2-4-fix-unset-attribute-reference/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.1.2-4-fix-unset-attribute-reference/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | // pre-add an attribute for quick testing: 10 | rental.settings["attributes"].push({ 11 | concept: "Data Attribute", 12 | settings: { 13 | "name": "new attribute", 14 | "type": "amount", 15 | // emulate "+ initial value" already being clicked, and Attribute Reference already chosen: 16 | "initial value": { 17 | "concept": "Attribute Reference", 18 | "settings": {} 19 | } 20 | } 21 | }) 22 | 23 | import { Projection } from "./projection" 24 | 25 | createRoot(document.getElementById("root")) 26 | .render( 27 | 31 | ) 32 | 33 | -------------------------------------------------------------------------------- /ch06/6.1.2-4-fix-unset-attribute-reference/support-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | 4 | 5 | export const AddNewButton = ({ buttonText, actionFunction }) => 6 | 13 | 14 | -------------------------------------------------------------------------------- /ch06/6.2.1-selecting-an-attribute/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch06/6.2.1-selecting-an-attribute/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.2.1-selecting-an-attribute/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /ch06/6.2.1-selecting-an-attribute/support-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | 4 | 5 | export const AddNewButton = ({ buttonText, actionFunction }) => 6 | 15 | 16 | -------------------------------------------------------------------------------- /ch06/6.2.2-deselection/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch06/6.2.2-deselection/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.2.2-deselection/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /ch06/6.2.2-deselection/support-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | 4 | 5 | export const AddNewButton = ({ buttonText, actionFunction }) => 6 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.2.3-1-wrapper-component/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch06/6.2.3-1-wrapper-component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.2.3-1-wrapper-component/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /ch06/6.2.3-1-wrapper-component/support-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action, observable } from "mobx" 3 | import { observer } from "mobx-react" 4 | 5 | import { asClassNameArgument } from "./css-util" 6 | 7 | 8 | const selection = observable({ selected: undefined }) 9 | 10 | const deselect = () => { 11 | selection.selected = undefined 12 | } 13 | 14 | document.addEventListener("mousedown", action((event) => { 15 | if (!event.target.classList.contains("selectable")) { 16 | deselect() 17 | } 18 | })) 19 | 20 | 21 | // A component that wraps the projection of an AST object ('astObject'): 22 | export const AstObjectUiWrapper = observer(({ className, astObject, children }) => { 23 | return
{ 26 | event.stopPropagation() 27 | selection.selected = astObject 28 | })} 29 | > 30 | {children} 31 |
32 | }) 33 | 34 | 35 | export const AddNewButton = ({ buttonText, actionFunction }) => 36 | 44 | 45 | -------------------------------------------------------------------------------- /ch06/6.2.3-2-all-selectable/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch06/6.2.3-2-all-selectable/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.2.3-2-all-selectable/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /ch06/6.2.3-2-all-selectable/support-components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action, observable } from "mobx" 3 | import { observer } from "mobx-react" 4 | 5 | import { asClassNameArgument } from "./css-util" 6 | 7 | 8 | const selection = observable({ selected: undefined }) 9 | 10 | const deselect = () => { 11 | selection.selected = undefined 12 | } 13 | 14 | document.addEventListener("mousedown", action((event) => { 15 | if (!event.target.classList.contains("selectable")) { 16 | deselect() 17 | } 18 | })) 19 | 20 | 21 | export const AstObjectUiWrapper = observer(({ className, astObject, children }) => { 22 | return
{ 25 | event.stopPropagation() 26 | selection.selected = astObject 27 | })} 28 | > 29 | {children} 30 |
31 | }) 32 | 33 | 34 | export const AddNewButton = ({ buttonText, actionFunction }) => 35 | 43 | 44 | -------------------------------------------------------------------------------- /ch06/6.3.1-1-catch-keys/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch06/6.3.1-1-catch-keys/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.3.1-1-catch-keys/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /ch06/6.3.1-2-delete-initial-values/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch06/6.3.1-2-delete-initial-values/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.3.1-2-delete-initial-values/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /ch06/6.3.2-delete-attributes/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch06/6.3.2-delete-attributes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch06/6.3.2-delete-attributes/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { observable } from "mobx" 4 | 5 | require("./styling.css") 6 | 7 | import rental from "../../ch03/rental-AST" 8 | 9 | import { Projection } from "./projection" 10 | 11 | createRoot(document.getElementById("root")) 12 | .render( 13 | 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /ch06/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | The DSL-*aspecific* code used by the frontends - currently only `ast.js` - is located in the `common/` directory. 4 | Each of the other directories contains a version of the frontend. 5 | The names indicate a section (§ .) of the chapter, usually (but optionally) followed by a hyphen and a further indication of the intention of that version. 6 | That indication can itself be numbered again, so the entire format for the directory names is: `.[-[-]]` 7 | 8 | Each of the frontend versions can be run by running the following commandline command 9 | 10 | ```shell 11 | $ npx parcel /index.html 12 | ``` 13 | 14 | and opening [`http://localhost:1234/`](http://localhost:1234/) in a browser. 15 | 16 | -------------------------------------------------------------------------------- /ch06/common/ast.js: -------------------------------------------------------------------------------- 1 | const isObject = (value) => (!!value) && (typeof value === "object") && !Array.isArray(value) 2 | 3 | const isAstObject = (value) => isObject(value) && ("concept" in value) && ("settings" in value) 4 | module.exports.isAstObject = isAstObject 5 | 6 | 7 | const isAstReferenceObject = (value) => isObject(value) && ("ref" in value) 8 | 9 | const isAstReference = (value) => isAstReferenceObject(value) && isAstObject(value.ref) 10 | module.exports.isAstReference = isAstReference 11 | 12 | 13 | const placeholderAstObject = "" 14 | module.exports.placeholderAstObject = placeholderAstObject 15 | 16 | -------------------------------------------------------------------------------- /ch07/backend/.gitignore: -------------------------------------------------------------------------------- 1 | /contents.json 2 | -------------------------------------------------------------------------------- /ch07/backend/data/.gitignore: -------------------------------------------------------------------------------- 1 | /contents.json 2 | -------------------------------------------------------------------------------- /ch07/backend/deserialize-contents.js: -------------------------------------------------------------------------------- 1 | const { readContents } = require("./storage") 2 | const { deserialize } = require("../common/ast") 3 | 4 | const serializedAst = readContents() 5 | const deserializedAst = deserialize(serializedAst) 6 | require("../../ch03/print-pretty")(deserializedAst) 7 | 8 | -------------------------------------------------------------------------------- /ch07/backend/server.js: -------------------------------------------------------------------------------- 1 | const { readContents, writeContents } = require("./storage") 2 | let contents = readContents() 3 | 4 | const express = require("express") 5 | const server = express() 6 | 7 | server.get("/contents", (request, response) => { 8 | response.json(contents) 9 | }) 10 | 11 | server.use(express.json({ limit: "1gb" })) 12 | server.put("/contents", (request, response) => { 13 | const newContents = request.body 14 | writeContents(newContents) 15 | contents = newContents 16 | response.send() 17 | }) 18 | 19 | const { join } = require("path") 20 | server.use(express.static(join(__dirname, "..", "dist"))) 21 | 22 | const port = 8080 23 | server.listen(port, () => { 24 | console.log(`Server started on: http://localhost:${port}/`) 25 | }) 26 | 27 | -------------------------------------------------------------------------------- /ch07/backend/storage.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path") 2 | 3 | const { readJson, writeJson } = require("../common/file-utils") 4 | 5 | 6 | const contentsPath = join(__dirname, "data", "contents.json") 7 | 8 | const readContents = () => readJson(contentsPath) 9 | module.exports.readContents = readContents 10 | 11 | const writeContents = (contents) => writeJson(contentsPath, contents) 12 | module.exports.writeContents = writeContents 13 | 14 | -------------------------------------------------------------------------------- /ch07/backend/test-POST-not-implemented.sh: -------------------------------------------------------------------------------- 1 | curl -X POST http://localhost:8080/contents 2 | -------------------------------------------------------------------------------- /ch07/common/file-utils.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require("fs") 2 | 3 | const writeString = (path, data) => { 4 | writeFileSync(path, data) 5 | } 6 | module.exports.writeString = writeString 7 | 8 | 9 | const writeJson = (path, data) => { 10 | writeString(path, JSON.stringify(data, null, 2)) 11 | } 12 | module.exports.writeJson = writeJson 13 | 14 | 15 | const readJson = (path) => 16 | JSON.parse(readFileSync(path, { encoding: "utf8" }).toString()) 17 | module.exports.readJson = readJson 18 | 19 | -------------------------------------------------------------------------------- /ch07/frontend/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch07/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch07/frontend/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { action, observable } from "mobx" 4 | import { observer } from "mobx-react" 5 | 6 | require("./styling.css") 7 | 8 | 9 | import { deserializeObservably, serialize } from "../common/ast" 10 | 11 | const state = observable({ 12 | ast: null 13 | }) 14 | 15 | const apiUrl = "http://localhost:8080/contents" 16 | 17 | fetch(apiUrl) 18 | .then((response) => response.json()) 19 | .then(action((json) => { 20 | state.ast = deserializeObservably(json) 21 | })) 22 | 23 | const save = (_) => { 24 | fetch(apiUrl, { 25 | method: "PUT", 26 | headers: { 27 | "Content-Type": "application/json" 28 | }, 29 | body: JSON.stringify(serialize(state.ast)) 30 | }) 31 | // (ignore returned Promise) 32 | } 33 | 34 | 35 | import { Projection } from "./projection" 36 | 37 | const App = observer(({ state }) => 38 | state.ast 39 | ?
40 | 41 | 45 |
46 | :
47 | ) 48 | 49 | createRoot(document.getElementById("root")) 50 | .render( 51 | 52 | ) 53 | 54 | -------------------------------------------------------------------------------- /ch07/init/example-AST.js: -------------------------------------------------------------------------------- 1 | const { newAstObject, astReferenceTo } = require("../common/ast") 2 | 3 | 4 | const rentalPeriodAttribute = newAstObject("Data Attribute", { 5 | "name": "rental period", 6 | "type": "date range" 7 | }) 8 | 9 | const rentalPriceBeforeDiscountAttribute = newAstObject("Data Attribute", { 10 | "name": "rental price before discount", 11 | "type": "amount", 12 | "initial value": newAstObject("Number", { 13 | "value": "0.0" 14 | }) 15 | }) 16 | 17 | const discountAttribute = newAstObject("Data Attribute", { 18 | "name": "discount", 19 | "type": "percentage", 20 | "initial value": newAstObject("Number", { 21 | "value": "0" 22 | }) 23 | }) 24 | 25 | const rentalPriceAfterDiscountAttribute = newAstObject("Data Attribute", { 26 | "name": "rental price after discount", 27 | "type": "amount", 28 | "initial value": newAstObject("Attribute Reference", { 29 | "attribute": astReferenceTo(rentalPriceBeforeDiscountAttribute) 30 | }) 31 | }) 32 | 33 | 34 | const rental = newAstObject("Record Type", { 35 | "name": "Rental", 36 | "attributes": [ 37 | rentalPeriodAttribute, 38 | rentalPriceBeforeDiscountAttribute, 39 | discountAttribute, 40 | rentalPriceAfterDiscountAttribute 41 | ] 42 | }) 43 | 44 | 45 | module.exports = rental 46 | 47 | -------------------------------------------------------------------------------- /ch07/init/example-contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Ceci n'est pas un arbre." 3 | } 4 | -------------------------------------------------------------------------------- /ch07/init/install-example-DSL-content.js: -------------------------------------------------------------------------------- 1 | const { writeContents } = require("../backend/storage") 2 | const { serialize } = require("../common/ast") 3 | const rental = require("./example-AST") 4 | 5 | writeContents(serialize(rental)) 6 | 7 | -------------------------------------------------------------------------------- /ch07/init/jsonify-Rental.js: -------------------------------------------------------------------------------- 1 | const { writeContents } = require("../backend/storage") 2 | const rental = require("../../ch03/rental-AST") 3 | 4 | writeContents(rental) 5 | 6 | -------------------------------------------------------------------------------- /ch07/init/put-example.sh: -------------------------------------------------------------------------------- 1 | curl -X PUT -d @init/example-contents.json http://localhost:8080/contents -H "Content-Type: application/json" 2 | -------------------------------------------------------------------------------- /ch07/initialize-storage.sh: -------------------------------------------------------------------------------- 1 | node init/install-example-DSL-content.js 2 | -------------------------------------------------------------------------------- /ch07/run-Domain-IDE.sh: -------------------------------------------------------------------------------- 1 | echo "Wait a couple of seconds for the Domain IDE to start, before reloading the opened browser tab:" 2 | open http://localhost:8080/ 3 | npx parcel frontend/index.html & 4 | node backend/server.js 5 | -------------------------------------------------------------------------------- /ch07/test/test-serialization.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NOT intended to go in the book's text. 3 | */ 4 | 5 | const { deepEqual, isTrue } = require("chai").assert 6 | 7 | 8 | const { deserialize, serialize } = require("../common/ast") 9 | 10 | // non-exported functions copied from ../common/ast.js: 11 | const isObject = (value) => (!!value) && (typeof value === "object") && !Array.isArray(value) 12 | const isAstReferenceObject = (value) => isObject(value) && ("ref" in value) 13 | const isSerializedAstReference = (value) => isObject(value) && ("refId" in value) 14 | 15 | 16 | describe("(-de)serialization can deal with target-less references", () => { 17 | 18 | const refObj = { ref: undefined } 19 | 20 | it("serialization recognizes target-less reference objects", () => { 21 | isTrue(isAstReferenceObject(refObj)) 22 | }) 23 | 24 | it("serialization should (de-)serialize a target-less reference correctly", () => { 25 | const serializedRefObject = serialize(refObj) 26 | deepEqual(serializedRefObject, { refId: undefined }) 27 | isTrue(isSerializedAstReference(serializedRefObject)) 28 | const deserializedRefObject = deserialize(serializedRefObject) 29 | deepEqual(deserializedRefObject, { ref: undefined }) 30 | isTrue(isAstReferenceObject(deserializedRefObject)) 31 | }) 32 | 33 | }) 34 | 35 | -------------------------------------------------------------------------------- /ch08/backend/.gitignore: -------------------------------------------------------------------------------- 1 | /contents.json 2 | -------------------------------------------------------------------------------- /ch08/backend/data/.gitignore: -------------------------------------------------------------------------------- 1 | /contents.json 2 | -------------------------------------------------------------------------------- /ch08/backend/server.js: -------------------------------------------------------------------------------- 1 | const { readContents, writeContents } = require("./storage") 2 | let contents = readContents() 3 | 4 | const express = require("express") 5 | const server = express() 6 | 7 | server.get("/contents", (request, response) => { 8 | response.json(contents) 9 | }) 10 | 11 | server.use(express.json({ limit: "1gb" })) 12 | server.put("/contents", (request, response) => { 13 | const newContents = request.body 14 | writeContents(newContents) 15 | contents = newContents 16 | response.send() 17 | }) 18 | 19 | // endpoint to generate src/runtime/index.jsx from contents: 20 | const { generatedIndexJsx } = require("../generator/indexJsx-template") 21 | const { deserialize } = require("../common/ast") 22 | server.get("/contents/indexJsx", (request, response) => { 23 | response.set("Content-Type", "text/plain") 24 | response.send(generatedIndexJsx(deserialize(contents))) 25 | }) 26 | 27 | const { join } = require("path") 28 | server.use(express.static(join(__dirname, "..", "dist"))) 29 | 30 | const port = 8080 31 | server.listen(port, () => { 32 | console.log(`Server started on: http://localhost:${port}/`) 33 | }) 34 | 35 | -------------------------------------------------------------------------------- /ch08/backend/storage.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path") 2 | 3 | const { readJson, writeJson } = require("../common/file-utils") 4 | 5 | 6 | const contentsPath = join(__dirname, "data", "contents.json") 7 | 8 | const readContents = () => readJson(contentsPath) 9 | module.exports.readContents = readContents 10 | 11 | const writeContents = (contents) => writeJson(contentsPath, contents) 12 | module.exports.writeContents = writeContents 13 | 14 | -------------------------------------------------------------------------------- /ch08/common/file-utils.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require("fs") 2 | 3 | const writeString = (path, data) => { 4 | writeFileSync(path, data) 5 | } 6 | module.exports.writeString = writeString 7 | 8 | 9 | const writeJson = (path, data) => { 10 | writeString(path, JSON.stringify(data, null, 2)) 11 | } 12 | module.exports.writeJson = writeJson 13 | 14 | 15 | const readJson = (path) => 16 | JSON.parse(readFileSync(path, { encoding: "utf8" }).toString()) 17 | module.exports.readJson = readJson 18 | 19 | -------------------------------------------------------------------------------- /ch08/frontend/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch08/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch08/frontend/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { action, observable } from "mobx" 4 | import { observer } from "mobx-react" 5 | 6 | require("./styling.css") 7 | 8 | 9 | import { deserializeObservably, serialize } from "../common/ast" 10 | 11 | const state = observable({ 12 | ast: null 13 | }) 14 | 15 | const apiUrl = "http://localhost:8080/contents" 16 | 17 | fetch(apiUrl) 18 | .then((response) => response.json()) 19 | .then(action((json) => { 20 | state.ast = deserializeObservably(json) 21 | })) 22 | 23 | const save = (_) => { 24 | fetch(apiUrl, { 25 | method: "PUT", 26 | headers: { 27 | "Content-Type": "application/json" 28 | }, 29 | body: JSON.stringify(serialize(state.ast)) 30 | }) 31 | // (ignore returned Promise) 32 | } 33 | 34 | 35 | import { Projection } from "./projection" 36 | 37 | const App = observer(({ state }) => 38 | state.ast 39 | ?
40 | 41 | 45 |
46 | :
47 | ) 48 | 49 | createRoot(document.getElementById("root")) 50 | .render( 51 | 52 | ) 53 | 54 | -------------------------------------------------------------------------------- /ch08/generate-and-run-Runtime.sh: -------------------------------------------------------------------------------- 1 | node generator/generate.js 2 | echo "Wait a couple of seconds for the Runtime to start, before reloading the opened browser tab:" 3 | open http://localhost:8180 4 | npx parcel runtime/index.html --port 8180 --dist-dir dist-runtime 5 | -------------------------------------------------------------------------------- /ch08/generator/generate-retrieve-ast-from-backend.js: -------------------------------------------------------------------------------- 1 | const http = require("http") 2 | const { join } = require("path") 3 | 4 | const { deserialize } = require("../common/ast") 5 | const { writeString } = require("../common/file-utils") 6 | const { generatedIndexJsx } = require("./generator") 7 | 8 | const indexJsxPath = join(__dirname, "..", "runtime", "index.jsx") 9 | 10 | http.request({ 11 | hostname: "localhost", 12 | port: 8080, 13 | path: "/contents", 14 | method: "GET" 15 | }, (response) => { 16 | let serializedAst = "" 17 | response.on("data", (chunk) => { 18 | serializedAst += chunk 19 | }) 20 | response.on("end", () => { 21 | const ast = deserialize(JSON.parse(serializedAst)) 22 | writeString(indexJsxPath, generatedIndexJsx(ast)) 23 | }) 24 | }).end() 25 | 26 | -------------------------------------------------------------------------------- /ch08/generator/generate.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path") 2 | 3 | const { deserialize } = require("../common/ast") 4 | const { writeString } = require("../common/file-utils") 5 | const { readContents } = require("../backend/storage") 6 | 7 | /* 8 | * The following implements some convenience on top of the book's code, 9 | * so we can run intermediate versions of the final template code in indexJsx-template.js 10 | * by specifying an extra argument when running the generator as follows: 11 | * 12 | * node src/generator/generate.js indexJsx-01-verbatim-generation-target.js 13 | * 14 | * Note that the extra argument is the full filename of the intermediate version of indexJsx-template.js 15 | */ 16 | const templateCliArgument = process.argv[2] 17 | const templateFile = "./" + (templateCliArgument ? templateCliArgument.substring(0, templateCliArgument.lastIndexOf(".js")) : "indexJsx-template") 18 | 19 | const { generatedIndexJsx } = require(templateFile) 20 | 21 | const indexJsxPath = join(__dirname, "..", "runtime", "index.jsx") 22 | 23 | const serializedAst = readContents() 24 | const deserializedAst = deserialize(serializedAst) 25 | writeString(indexJsxPath, generatedIndexJsx(deserializedAst)) 26 | 27 | -------------------------------------------------------------------------------- /ch08/init/example-AST.js: -------------------------------------------------------------------------------- 1 | const { newAstObject, astReferenceTo } = require("../common/ast") 2 | 3 | 4 | const rentalPeriodAttribute = newAstObject("Data Attribute", { 5 | "name": "rental period", 6 | "type": "date range" 7 | }) 8 | 9 | const rentalPriceBeforeDiscountAttribute = newAstObject("Data Attribute", { 10 | "name": "rental price before discount", 11 | "type": "amount", 12 | "initial value": newAstObject("Number", { 13 | "value": "0.0" 14 | }) 15 | }) 16 | 17 | const discountAttribute = newAstObject("Data Attribute", { 18 | "name": "discount", 19 | "type": "percentage", 20 | "initial value": newAstObject("Number", { 21 | "value": "0" 22 | }) 23 | }) 24 | 25 | const rentalPriceAfterDiscountAttribute = newAstObject("Data Attribute", { 26 | "name": "rental price after discount", 27 | "type": "amount", 28 | "initial value": newAstObject("Attribute Reference", { 29 | "attribute": astReferenceTo(rentalPriceBeforeDiscountAttribute) 30 | }) 31 | }) 32 | 33 | 34 | const rental = newAstObject("Record Type", { 35 | "name": "Rental", 36 | "attributes": [ 37 | rentalPeriodAttribute, 38 | rentalPriceBeforeDiscountAttribute, 39 | discountAttribute, 40 | rentalPriceAfterDiscountAttribute 41 | ] 42 | }) 43 | 44 | 45 | module.exports = rental 46 | 47 | -------------------------------------------------------------------------------- /ch08/init/install-example-DSL-content.js: -------------------------------------------------------------------------------- 1 | const { writeContents } = require("../backend/storage") 2 | const { serialize } = require("../common/ast") 3 | const rental = require("./example-AST") 4 | 5 | writeContents(serialize(rental)) 6 | 7 | -------------------------------------------------------------------------------- /ch08/initialize-storage.sh: -------------------------------------------------------------------------------- 1 | node init/install-example-DSL-content.js 2 | -------------------------------------------------------------------------------- /ch08/run-Domain-IDE.sh: -------------------------------------------------------------------------------- 1 | echo "Wait a couple of seconds for the Domain IDE to start, before reloading the opened browser tab:" 2 | open http://localhost:8080/ 3 | npx parcel frontend/index.html & 4 | node backend/server.js 5 | -------------------------------------------------------------------------------- /ch08/runtime/components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | import { observer } from "mobx-react" 4 | 5 | import { formatDate } from "./dates" 6 | 7 | 8 | export const FormField = observer(({ label, children }) =>
9 | 10 |
11 | {children} 12 |
13 |
) 14 | 15 | 16 | const convertToType = (newValue /*: string from onChange's event.target.value */, type) => { 17 | switch (type) { 18 | case "number": return Number.parseFloat(newValue) 19 | case "date": return new Date(newValue) 20 | default: return newValue 21 | } 22 | } 23 | 24 | const convertFromType = (value, type) => { 25 | switch (type) { 26 | case "number": return "" + value 27 | case "date": return formatDate(value) 28 | default: return value 29 | } 30 | } 31 | 32 | export const Input = observer(({ type, object, fieldName }) => { 36 | object[fieldName] = convertToType(event.target.value, type) 37 | })} 38 | />) 39 | 40 | /* 41 | * Note: HTML's input element uses the format "yyyy-mm-dd" when type="date" 42 | */ 43 | 44 | -------------------------------------------------------------------------------- /ch08/runtime/dates.js: -------------------------------------------------------------------------------- 1 | const { makeAutoObservable } = require("mobx") 2 | 3 | 4 | const leftPad0 = (num, len) => { 5 | let str = "" + num 6 | if (str.length < len) { 7 | str = "0".repeat(len - str.length) + str 8 | } 9 | return str 10 | } 11 | const formatDate = (date) => `${leftPad0(date.getFullYear(), 4)}-${leftPad0(date.getMonth() + 1, 2)}-${leftPad0(date.getDate(), 2)}` 12 | // date.toString().substring(0, 10) doesn't work... 13 | module.exports.formatDate = formatDate 14 | 15 | 16 | class DateRange { 17 | _from; 18 | _to; 19 | get from() { 20 | return this._from 21 | } 22 | set from(newValue) { 23 | this._from = newValue 24 | if (this._to < this._from) { 25 | this._to = this._from 26 | } 27 | } 28 | get to() { 29 | return this._to 30 | } 31 | set to(newValue) { 32 | this._to = newValue 33 | if (this._from > this._to) { 34 | this._from = this._to 35 | } 36 | } 37 | toString() { 38 | return `${formatDate(this._from)} - ${formatDate(this._to)}` 39 | } 40 | constructor(fromStr, toStr) { 41 | makeAutoObservable(this) 42 | const now = new Date() 43 | this._from = !!fromStr ? new Date(fromStr) : now 44 | this._to = !!toStr ? new Date(toStr) : now 45 | } 46 | } 47 | module.exports.DateRange = DateRange 48 | 49 | -------------------------------------------------------------------------------- /ch08/runtime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch08/runtime/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { makeAutoObservable } from "mobx" 4 | import { observer } from "mobx-react" 5 | 6 | import { FormField, Input } from "./components" 7 | import { DateRange } from "./dates" 8 | 9 | require("./styling.css") 10 | 11 | class Rental { 12 | rentalPeriod = new DateRange() 13 | rentalPriceBeforeDiscount = 0.0 14 | discount = 0 15 | rentalPriceAfterDiscount = this.rentalPriceBeforeDiscount 16 | constructor() { 17 | makeAutoObservable(this) 18 | } 19 | } 20 | 21 | const RentalForm = observer(({ rental }) =>
22 | 23 | 24 | 25 | 26 | 27 | $ 28 | 29 | 30 | % 31 | 32 | 33 | $ 34 | 35 |
) 36 | 37 | const rental = new Rental() 38 | 39 | createRoot(document.getElementById("root")) 40 | .render( 41 | 42 | ) 43 | 44 | -------------------------------------------------------------------------------- /ch08/runtime/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | form { 7 | display: grid; 8 | grid-gap: 0.5em; 9 | } 10 | 11 | div.row { 12 | display: grid; 13 | grid-template-columns: 1fr 1fr; 14 | } 15 | 16 | label:after { 17 | content: ":"; 18 | grid-column: 1 / 2; 19 | } 20 | 21 | div.field { 22 | margin-left: 0.5rem; 23 | grid-column: 2 / 3; 24 | } 25 | 26 | input { 27 | font-size: 18pt; 28 | border: 3px solid yellow; 29 | background-color: lightyellow; 30 | } 31 | 32 | input[type="date"] { 33 | margin-right: 0.3em; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ch09/backend/data/.gitignore: -------------------------------------------------------------------------------- 1 | /contents.json 2 | -------------------------------------------------------------------------------- /ch09/backend/server.js: -------------------------------------------------------------------------------- 1 | const { readContents, writeContents } = require("./storage") 2 | let contents = readContents() 3 | 4 | const express = require("express") 5 | const server = express() 6 | 7 | server.get("/contents", (request, response) => { 8 | response.json(contents) 9 | }) 10 | 11 | server.use(express.json({ limit: "1gb" })) 12 | server.put("/contents", (request, response) => { 13 | const newContents = request.body 14 | writeContents(newContents) 15 | contents = newContents 16 | response.send() 17 | }) 18 | 19 | // endpoint to generate src/runtime/index.jsx from contents: 20 | const { generatedIndexJsx } = require("../generator/indexJsx-template") 21 | const { deserialize } = require("../common/ast") 22 | server.get("/contents/indexJsx", (request, response) => { 23 | response.set("Content-Type", "text/plain") 24 | response.send(generatedIndexJsx(deserialize(contents))) 25 | }) 26 | 27 | const { join } = require("path") 28 | server.use(express.static(join(__dirname, "..", "dist"))) 29 | 30 | const port = 8080 31 | server.listen(port, () => { 32 | console.log(`Server started on: http://localhost:${port}/`) 33 | }) 34 | 35 | -------------------------------------------------------------------------------- /ch09/backend/storage.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path") 2 | 3 | const { readJson, writeJson } = require("../common/file-utils") 4 | 5 | 6 | const contentsPath = join(__dirname, "data", "contents.json") 7 | 8 | const readContents = () => readJson(contentsPath) 9 | module.exports.readContents = readContents 10 | 11 | const writeContents = (contents) => writeJson(contentsPath, contents) 12 | module.exports.writeContents = writeContents 13 | 14 | -------------------------------------------------------------------------------- /ch09/common/file-utils.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require("fs") 2 | 3 | const writeString = (path, data) => { 4 | writeFileSync(path, data) 5 | } 6 | module.exports.writeString = writeString 7 | 8 | 9 | const writeJson = (path, data) => { 10 | writeString(path, JSON.stringify(data, null, 2)) 11 | } 12 | module.exports.writeJson = writeJson 13 | 14 | 15 | const readJson = (path) => 16 | JSON.parse(readFileSync(path, { encoding: "utf8" }).toString()) 17 | module.exports.readJson = readJson 18 | 19 | -------------------------------------------------------------------------------- /ch09/frontend/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch09/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch09/frontend/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { action, observable } from "mobx" 4 | import { observer } from "mobx-react" 5 | 6 | require("./styling.css") 7 | 8 | 9 | import { deserializeObservably, serialize } from "../common/ast" 10 | 11 | const state = observable({ 12 | ast: null 13 | }) 14 | 15 | const apiUrl = "http://localhost:8080/contents" 16 | 17 | fetch(apiUrl) 18 | .then((response) => response.json()) 19 | .then(action((json) => { 20 | state.ast = deserializeObservably(json) 21 | })) 22 | 23 | const save = (_) => { 24 | fetch(apiUrl, { 25 | method: "PUT", 26 | headers: { 27 | "Content-Type": "application/json" 28 | }, 29 | body: JSON.stringify(serialize(state.ast)) 30 | }) 31 | // (ignore returned Promise) 32 | } 33 | 34 | 35 | import { Projection } from "./projection" 36 | 37 | const App = observer(({ state }) => 38 | state.ast 39 | ?
40 | 41 | 45 |
46 | :
47 | ) 48 | 49 | createRoot(document.getElementById("root")) 50 | .render( 51 | 52 | ) 53 | 54 | -------------------------------------------------------------------------------- /ch09/generate-and-run-Runtime.sh: -------------------------------------------------------------------------------- 1 | node generator/generate.js 2 | echo "Wait a couple of seconds for the Runtime to start, before reloading the opened browser tab:" 3 | open http://localhost:8180 4 | npx parcel runtime/index.html --port 8180 --dist-dir dist-runtime 5 | -------------------------------------------------------------------------------- /ch09/generator/generate.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path") 2 | 3 | const { deserialize, isAstObject } = require("../common/ast") 4 | const { writeString } = require("../common/file-utils") 5 | const { readContents } = require("../backend/storage") 6 | const { issuesFor } = require("../language/constraints") 7 | const { generatedIndexJsx } = require("./indexJsx-template") 8 | 9 | const indexJsxPath = join(__dirname, "..", "runtime", "index.jsx") 10 | 11 | const serializedAst = readContents() 12 | const deserializedAst = deserialize(serializedAst) 13 | 14 | 15 | // Exercise 9.3 - print all issues: 16 | 17 | const printIssue = (issue, astObject) => { 18 | console.log(`[ERROR] on AST object with id='${astObject.id}', concept='${astObject.concept}'; message: "${issue}"`) 19 | } 20 | 21 | const printAllIssuesFor = (astObject, ancestors) => { 22 | if (isAstObject(astObject)) { 23 | issuesFor(astObject, ancestors).forEach((issue) => printIssue(issue, astObject)) 24 | for (const propertyName in astObject.settings) { 25 | printAllIssuesFor(astObject.settings[propertyName], [ astObject, ...ancestors ]) 26 | } 27 | } 28 | if (Array.isArray(astObject)) { 29 | astObject.forEach((item) => printAllIssuesFor(item, ancestors)) 30 | } 31 | } 32 | 33 | printAllIssuesFor(deserializedAst, []) 34 | 35 | 36 | writeString(indexJsxPath, generatedIndexJsx(deserializedAst)) 37 | 38 | -------------------------------------------------------------------------------- /ch09/init/example-AST.js: -------------------------------------------------------------------------------- 1 | const { newAstObject, astReferenceTo } = require("../common/ast") 2 | 3 | 4 | const rentalPeriodAttribute = newAstObject("Data Attribute", { 5 | "name": "rental period", 6 | "type": "date range" 7 | }) 8 | 9 | const rentalPriceBeforeDiscountAttribute = newAstObject("Data Attribute", { 10 | "name": "rental price before discount", 11 | "type": "amount", 12 | "initial value": newAstObject("Number", { 13 | "value": "0.0" 14 | }) 15 | }) 16 | 17 | const discountAttribute = newAstObject("Data Attribute", { 18 | "name": "discount", 19 | "type": "percentage", 20 | "initial value": newAstObject("Number", { 21 | "value": "0" 22 | }) 23 | }) 24 | 25 | const rentalPriceAfterDiscountAttribute = newAstObject("Data Attribute", { 26 | "name": "rental price after discount", 27 | "type": "amount", 28 | "initial value": newAstObject("Attribute Reference", { 29 | "attribute": astReferenceTo(rentalPriceBeforeDiscountAttribute) 30 | }) 31 | }) 32 | 33 | 34 | const rental = newAstObject("Record Type", { 35 | "name": "Rental", 36 | "attributes": [ 37 | rentalPeriodAttribute, 38 | rentalPriceBeforeDiscountAttribute, 39 | discountAttribute, 40 | rentalPriceAfterDiscountAttribute 41 | ] 42 | }) 43 | 44 | 45 | module.exports = rental 46 | 47 | -------------------------------------------------------------------------------- /ch09/init/install-example-DSL-content.js: -------------------------------------------------------------------------------- 1 | const { writeContents } = require("../backend/storage") 2 | const { serialize } = require("../common/ast") 3 | const rental = require("./example-AST") 4 | 5 | writeContents(serialize(rental)) 6 | 7 | -------------------------------------------------------------------------------- /ch09/initialize-storage.sh: -------------------------------------------------------------------------------- 1 | node init/install-example-DSL-content.js 2 | -------------------------------------------------------------------------------- /ch09/language/queries.js: -------------------------------------------------------------------------------- 1 | const { isAstObject, isAstReference } = require("../common/ast") 2 | 3 | /** 4 | * Computes all the attributes referenced anywhere within the value of the given `attribute`. 5 | * @param attribute An AST object with concept label "Data Attribute". 6 | * @return An array of attribute AST objects - possibly empty. 7 | */ 8 | const referencedAttributesInValueOf = (attribute) => { 9 | const initialValue = attribute.settings["initial value"] 10 | if (isAstObject(initialValue) && initialValue.concept === "Attribute Reference") { 11 | const refObject = initialValue.settings["attribute"] 12 | return isAstReference(refObject) ? [ refObject.ref ] : [] 13 | } 14 | return [] 15 | } 16 | module.exports.referencedAttributesInValueOf = referencedAttributesInValueOf 17 | 18 | 19 | /** 20 | * @param astObject - an AST object with a string-valued "name" property. 21 | * @return the name of the given AST object (as a string), with (single) quotes around it. 22 | */ 23 | const nameOf = (astObject) => astObject.settings["name"] 24 | const quote = (str) => `'${str}'` 25 | /** 26 | * @param astObjects An array of AST objects with a string-valued "name" property. 27 | * @return An array of names (strings), with (single) quotes around them. 28 | */ 29 | const quotedNamesOf = (astObjects) => astObjects.map(nameOf).map(quote) 30 | module.exports.quotedNamesOf = quotedNamesOf 31 | 32 | -------------------------------------------------------------------------------- /ch09/run-Domain-IDE.sh: -------------------------------------------------------------------------------- 1 | echo "Wait a couple of seconds for the Domain IDE to start, before reloading the opened browser tab:" 2 | open http://localhost:8080/ 3 | npx parcel frontend/index.html & 4 | node backend/server.js 5 | -------------------------------------------------------------------------------- /ch09/runtime/components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | import { observer } from "mobx-react" 4 | 5 | import { formatDate } from "./dates" 6 | 7 | 8 | export const FormField = observer(({ label, children }) =>
9 | 10 |
11 | {children} 12 |
13 |
) 14 | 15 | 16 | const convertToType = (newValue /*: string from onChange's event.target.value */, type) => { 17 | switch (type) { 18 | case "number": return Number.parseFloat(newValue) 19 | case "date": return new Date(newValue) 20 | default: return newValue 21 | } 22 | } 23 | 24 | const convertFromType = (value, type) => { 25 | switch (type) { 26 | case "number": return "" + value 27 | case "date": return formatDate(value) 28 | default: return value 29 | } 30 | } 31 | 32 | export const Input = observer(({ type, object, fieldName }) => { 36 | object[fieldName] = convertToType(event.target.value, type) 37 | })} 38 | />) 39 | 40 | /* 41 | * Note: HTML's input element uses the format "yyyy-mm-dd" when type="date" 42 | */ 43 | 44 | -------------------------------------------------------------------------------- /ch09/runtime/dates.js: -------------------------------------------------------------------------------- 1 | const { makeAutoObservable } = require("mobx") 2 | 3 | 4 | const leftPad0 = (num, len) => { 5 | let str = "" + num 6 | if (str.length < len) { 7 | str = "0".repeat(len - str.length) + str 8 | } 9 | return str 10 | } 11 | const formatDate = (date) => `${leftPad0(date.getFullYear(), 4)}-${leftPad0(date.getMonth() + 1, 2)}-${leftPad0(date.getDate(), 2)}` 12 | // date.toString().substring(0, 10) doesn't work... 13 | module.exports.formatDate = formatDate 14 | 15 | 16 | class DateRange { 17 | _from; 18 | _to; 19 | get from() { 20 | return this._from 21 | } 22 | set from(newValue) { 23 | this._from = newValue 24 | if (this._to < this._from) { 25 | this._to = this._from 26 | } 27 | } 28 | get to() { 29 | return this._to 30 | } 31 | set to(newValue) { 32 | this._to = newValue 33 | if (this._from > this._to) { 34 | this._from = this._to 35 | } 36 | } 37 | toString() { 38 | return `${formatDate(this._from)} - ${formatDate(this._to)}` 39 | } 40 | constructor(fromStr, toStr) { 41 | makeAutoObservable(this) 42 | const now = new Date() 43 | this._from = !!fromStr ? new Date(fromStr) : now 44 | this._to = !!toStr ? new Date(toStr) : now 45 | } 46 | } 47 | module.exports.DateRange = DateRange 48 | 49 | -------------------------------------------------------------------------------- /ch09/runtime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch09/runtime/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { makeAutoObservable } from "mobx" 4 | import { observer } from "mobx-react" 5 | 6 | import { FormField, Input } from "./components" 7 | import { DateRange } from "./dates" 8 | 9 | require("./styling.css") 10 | 11 | class Rental { 12 | rentalPeriod = new DateRange() 13 | rentalPriceBeforeDiscount = 0.0 14 | discount = 0 15 | rentalPriceAfterDiscount = this.rentalPriceBeforeDiscount 16 | constructor() { 17 | makeAutoObservable(this) 18 | } 19 | } 20 | 21 | const RentalForm = observer(({ rental }) =>
22 | 23 | 24 | 25 | 26 | 27 | $ 28 | 29 | 30 | % 31 | 32 | 33 | $ 34 | 35 |
) 36 | 37 | const rental = new Rental() 38 | 39 | createRoot(document.getElementById("root")) 40 | .render( 41 | 42 | ) 43 | 44 | -------------------------------------------------------------------------------- /ch09/runtime/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | form { 7 | display: grid; 8 | grid-gap: 0.5em; 9 | } 10 | 11 | div.row { 12 | display: grid; 13 | grid-template-columns: 1fr 1fr; 14 | } 15 | 16 | label:after { 17 | content: ":"; 18 | grid-column: 1 / 2; 19 | } 20 | 21 | div.field { 22 | margin-left: 0.5rem; 23 | grid-column: 2 / 3; 24 | } 25 | 26 | input { 27 | font-size: 18pt; 28 | border: 3px solid yellow; 29 | background-color: lightyellow; 30 | } 31 | 32 | input[type="date"] { 33 | margin-right: 0.3em; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ch10/backend/data/.gitignore: -------------------------------------------------------------------------------- 1 | /contents*.json 2 | 3 | # for Exercise 10.4: 4 | /metaData.json 5 | 6 | -------------------------------------------------------------------------------- /ch10/backend/server.js: -------------------------------------------------------------------------------- 1 | const { readVersionedContents, writeContents } = require("./storage") 2 | 3 | const versionedContents = readVersionedContents() 4 | // for Exercise 10.4: 5 | const dslVersion = versionedContents.version // (This implies: no AST migrations while the server is running.) 6 | let contents = versionedContents.contents 7 | 8 | const express = require("express") 9 | const server = express() 10 | 11 | server.get("/contents", (request, response) => { 12 | // for Exercise 10.4: 13 | response.header("X-DSL-Version", dslVersion) 14 | response.json(contents) 15 | }) 16 | 17 | server.use(express.json({ limit: "1gb" })) 18 | server.put("/contents", (request, response) => { 19 | const newContents = request.body 20 | writeContents(newContents) 21 | contents = newContents 22 | response.send() 23 | }) 24 | 25 | // endpoint to generate src/runtime/index.jsx from contents: 26 | const { generatedIndexJsx } = require("../generator/indexJsx-template") 27 | const { deserialize } = require("../common/ast") 28 | server.get("/contents/indexJsx", (request, response) => { 29 | response.set("Content-Type", "text/plain") 30 | response.send(generatedIndexJsx(deserialize(contents))) 31 | }) 32 | 33 | const { join } = require("path") 34 | server.use(express.static(join(__dirname, "..", "dist"))) 35 | 36 | const port = 8080 37 | server.listen(port, () => { 38 | console.log(`Server started on: http://localhost:${port}/`) 39 | }) 40 | 41 | -------------------------------------------------------------------------------- /ch10/common/file-utils.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require("fs") 2 | 3 | const writeString = (path, data) => { 4 | writeFileSync(path, data) 5 | } 6 | module.exports.writeString = writeString 7 | 8 | 9 | const writeJson = (path, data) => { 10 | writeString(path, JSON.stringify(data, null, 2)) 11 | } 12 | module.exports.writeJson = writeJson 13 | 14 | 15 | const readJson = (path) => 16 | JSON.parse(readFileSync(path, { encoding: "utf8" }).toString()) 17 | module.exports.readJson = readJson 18 | 19 | -------------------------------------------------------------------------------- /ch10/exercise-10.3.md: -------------------------------------------------------------------------------- 1 | # Answer to exercise 10.3 2 | 3 | The following lists every source file of the Domain IDE, and whether it's DSL-_specific_, -_generic_, or -_aspecific_. 4 | 5 | | Folder | File | DSL- | 6 | | ------ | ---- | ---- | 7 | | `src/backend` | `server.js` | aspecific | 8 | | | `storage.js` | aspecific | 9 | | `src/common` | `ast.js` | generic | 10 | | | `dependency-utils.js` | aspecific* | 11 | | | `file-utils.js` | aspecific | 12 | | `src/frontend` | `css-util.js` | aspecific | 13 | | | `index.html` | aspecific | 14 | | | `index.jsx` | aspecific | 15 | | | `projection.jsx` | *specific* | 16 | | | `styling.css` | all three | 17 | | | `support-components.jsx` | generic | 18 | | | `value-components.jsx` | generic | 19 | | `src/generator` | `generate.js` | generic | 20 | | | `indexJsx-template.js` | *specific* | 21 | | | `template-utils.js` | aspecific | 22 | | `src/init` | `example-AST.js` | *specific* | 23 | | | `install-example-DSL-content.js` | *specific* | 24 | | | `migrations.js` | *specific* | 25 | | `src/language` | `constraints.js` | *specific* | 26 | | | `queries.js` | *specific* | 27 | 28 | *) after doing a proper Refactoring so that a dependencies function is passed as an argument, instead of using the `referencedAttributesInValueOf` function from `src/language/queries`. 29 | 30 | -------------------------------------------------------------------------------- /ch10/frontend/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch10/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch10/generate-and-run-Runtime.sh: -------------------------------------------------------------------------------- 1 | node generator/generate.js 2 | echo "Wait a couple of seconds for the Runtime to start, before reloading the opened browser tab:" 3 | open http://localhost:8180 4 | npx parcel runtime/index.html --port 8180 --dist-dir dist-runtime 5 | -------------------------------------------------------------------------------- /ch10/generator/generate.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path") 2 | 3 | const { deserialize, isAstObject } = require("../common/ast") 4 | const { writeString } = require("../common/file-utils") 5 | const { readVersionedContents } = require("../backend/storage") 6 | const { issuesFor } = require("../language/constraints") 7 | const { generatedIndexJsx } = require("./indexJsx-template") 8 | 9 | const indexJsxPath = join(__dirname, "..", "runtime", "index.jsx") 10 | 11 | const serializedAst = readVersionedContents().contents 12 | const deserializedAst = deserialize(serializedAst) 13 | 14 | 15 | const printIssue = (issue, astObject) => { 16 | console.log(`[ERROR] on AST object with id='${astObject.id}', concept='${astObject.concept}'; message: "${issue}"`) 17 | } 18 | 19 | const printAllIssuesFor = (astObject, ancestors) => { 20 | if (isAstObject(astObject)) { 21 | issuesFor(astObject, ancestors).forEach((issue) => printIssue(issue, astObject)) 22 | for (const propertyName in astObject.settings) { 23 | printAllIssuesFor(astObject.settings[propertyName], [ astObject, ...ancestors ]) 24 | } 25 | } 26 | if (Array.isArray(astObject)) { 27 | astObject.forEach((item) => printAllIssuesFor(item, ancestors)) 28 | } 29 | } 30 | 31 | printAllIssuesFor(deserializedAst, []) 32 | 33 | 34 | writeString(indexJsxPath, generatedIndexJsx(deserializedAst)) 35 | 36 | -------------------------------------------------------------------------------- /ch10/init/example-AST.js: -------------------------------------------------------------------------------- 1 | const { newAstObject, astReferenceTo } = require("../common/ast") 2 | 3 | 4 | const rentalPeriodAttribute = newAstObject("Data Attribute", { 5 | "name": "rental period", 6 | "type": "date range" 7 | }) 8 | 9 | const rentalPriceBeforeDiscountAttribute = newAstObject("Data Attribute", { 10 | "name": "rental price before discount", 11 | "type": "amount", 12 | "initial value": newAstObject("Number", { 13 | "value": "0.0" 14 | }) 15 | }) 16 | 17 | const discountAttribute = newAstObject("Data Attribute", { 18 | "name": "discount", 19 | "type": "percentage", 20 | "initial value": newAstObject("Number", { 21 | "value": "0" 22 | }) 23 | }) 24 | 25 | const rentalPriceAfterDiscountAttribute = newAstObject("Data Attribute", { 26 | "name": "rental price after discount", 27 | "type": "amount", 28 | "initial value": newAstObject("Attribute Reference", { 29 | "attribute": astReferenceTo(rentalPriceBeforeDiscountAttribute) 30 | }) 31 | }) 32 | 33 | 34 | const rental = newAstObject("Record Type", { 35 | "name": "Rental", 36 | "attributes": [ 37 | rentalPeriodAttribute, 38 | rentalPriceBeforeDiscountAttribute, 39 | discountAttribute, 40 | rentalPriceAfterDiscountAttribute 41 | ] 42 | }) 43 | 44 | 45 | module.exports = rental 46 | 47 | -------------------------------------------------------------------------------- /ch10/init/install-example-DSL-content.js: -------------------------------------------------------------------------------- 1 | const { writeVersionedContents } = require("../backend/storage") 2 | const { serialize } = require("../common/ast") 3 | const rental = require("./example-AST") 4 | 5 | writeVersionedContents(serialize(rental), "v1") 6 | 7 | -------------------------------------------------------------------------------- /ch10/initialize-storage.sh: -------------------------------------------------------------------------------- 1 | rm -f backend/data/*.json # also just delete _all_ JSON contents prior to initialization of the storage 2 | node init/install-example-DSL-content.js 3 | -------------------------------------------------------------------------------- /ch10/language/queries.js: -------------------------------------------------------------------------------- 1 | const { isAstObject, isAstReference } = require("../common/ast") 2 | 3 | /** 4 | * Computes all the attributes referenced anywhere within the value of the given `attribute`. 5 | * @param attribute An AST object with concept label "Attribute". 6 | * @return An array of attribute AST objects - possibly empty. 7 | */ 8 | const referencedAttributesInValueOf = (attribute) => { 9 | const value = attribute.settings["value"] 10 | if (isAstObject(value) && value.concept === "Attribute Reference") { 11 | const refObject = value.settings["attribute"] 12 | return isAstReference(refObject) ? [ refObject.ref ] : [] 13 | } 14 | return [] 15 | } 16 | module.exports.referencedAttributesInValueOf = referencedAttributesInValueOf 17 | 18 | 19 | /** 20 | * @param astObject - an AST object with a string-valued "name" property. 21 | * @return the name of the given AST object (as a string), with (single) quotes around it. 22 | */ 23 | const nameOf = (astObject) => astObject.settings["name"] 24 | const quote = (str) => `'${str}'` 25 | /** 26 | * @param astObjects An array of AST objects with a string-valued "name" property. 27 | * @return An array of names (strings), with (single) quotes around them. 28 | */ 29 | const quotedNamesOf = (astObjects) => astObjects.map(nameOf).map(quote) 30 | module.exports.quotedNamesOf = quotedNamesOf 31 | 32 | -------------------------------------------------------------------------------- /ch10/run-Domain-IDE.sh: -------------------------------------------------------------------------------- 1 | node init/migrations.js 2 | echo "Wait a couple of seconds for the Domain IDE to start, before reloading the opened browser tab:" 3 | open http://localhost:8080/ 4 | npx parcel frontend/index.html & 5 | node backend/server.js 6 | -------------------------------------------------------------------------------- /ch10/runtime/components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | import { observer } from "mobx-react" 4 | 5 | import { formatDate } from "./dates" 6 | 7 | 8 | export const FormField = observer(({ label, children }) =>
9 | 10 |
11 | {children} 12 |
13 |
) 14 | 15 | 16 | const convertToType = (newValue /*: string from onChange's event.target.value */, type) => { 17 | switch (type) { 18 | case "number": return Number.parseFloat(newValue) 19 | case "date": return new Date(newValue) 20 | default: return newValue 21 | } 22 | } 23 | 24 | const convertFromType = (value, type) => { 25 | switch (type) { 26 | case "number": return "" + value 27 | case "date": return formatDate(value) 28 | default: return value 29 | } 30 | } 31 | 32 | export const Input = observer(({ type, object, fieldName }) => { 36 | object[fieldName] = convertToType(event.target.value, type) 37 | })} 38 | />) 39 | 40 | /* 41 | * Note: HTML's input element uses the format "yyyy-mm-dd" when type="date" 42 | */ 43 | 44 | -------------------------------------------------------------------------------- /ch10/runtime/dates.js: -------------------------------------------------------------------------------- 1 | const { makeAutoObservable } = require("mobx") 2 | 3 | 4 | const leftPad0 = (num, len) => { 5 | let str = "" + num 6 | if (str.length < len) { 7 | str = "0".repeat(len - str.length) + str 8 | } 9 | return str 10 | } 11 | const formatDate = (date) => `${leftPad0(date.getFullYear(), 4)}-${leftPad0(date.getMonth() + 1, 2)}-${leftPad0(date.getDate(), 2)}` 12 | // date.toString().substring(0, 10) doesn't work... 13 | module.exports.formatDate = formatDate 14 | 15 | 16 | class DateRange { 17 | _from; 18 | _to; 19 | get from() { 20 | return this._from 21 | } 22 | set from(newValue) { 23 | this._from = newValue 24 | if (this._to < this._from) { 25 | this._to = this._from 26 | } 27 | } 28 | get to() { 29 | return this._to 30 | } 31 | set to(newValue) { 32 | this._to = newValue 33 | if (this._from > this._to) { 34 | this._from = this._to 35 | } 36 | } 37 | toString() { 38 | return `${formatDate(this._from)} - ${formatDate(this._to)}` 39 | } 40 | constructor(fromStr, toStr) { 41 | makeAutoObservable(this) 42 | const now = new Date() 43 | this._from = !!fromStr ? new Date(fromStr) : now 44 | this._to = !!toStr ? new Date(toStr) : now 45 | } 46 | } 47 | module.exports.DateRange = DateRange 48 | 49 | -------------------------------------------------------------------------------- /ch10/runtime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch10/runtime/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { makeAutoObservable } from "mobx" 4 | import { observer } from "mobx-react" 5 | 6 | import { FormField, Input } from "./components" 7 | import { DateRange } from "./dates" 8 | 9 | require("./styling.css") 10 | 11 | class Rental { 12 | rentalPeriod = new DateRange() 13 | rentalPriceBeforeDiscount = 0.0 14 | discount = 0 15 | rentalPriceAfterDiscount = this.rentalPriceBeforeDiscount 16 | constructor() { 17 | makeAutoObservable(this) 18 | } 19 | } 20 | 21 | const RentalForm = observer(({ rental }) =>
22 | 23 | 24 | 25 | 26 | 27 | $ 28 | 29 | 30 | % 31 | 32 | 33 | $ 34 | 35 |
) 36 | 37 | const rental = new Rental() 38 | 39 | createRoot(document.getElementById("root")) 40 | .render( 41 | 42 | ) 43 | 44 | -------------------------------------------------------------------------------- /ch10/runtime/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | form { 7 | display: grid; 8 | grid-gap: 0.5em; 9 | } 10 | 11 | div.row { 12 | display: grid; 13 | grid-template-columns: 1fr 1fr; 14 | } 15 | 16 | label:after { 17 | content: ":"; 18 | grid-column: 1 / 2; 19 | } 20 | 21 | div.field { 22 | margin-left: 0.5rem; 23 | grid-column: 2 / 3; 24 | } 25 | 26 | input { 27 | font-size: 18pt; 28 | border: 3px solid yellow; 29 | background-color: lightyellow; 30 | } 31 | 32 | input[type="date"] { 33 | margin-right: 0.3em; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ch11/backend/data/.gitignore: -------------------------------------------------------------------------------- 1 | /contents*.json 2 | /metaData.json 3 | -------------------------------------------------------------------------------- /ch11/backend/server.js: -------------------------------------------------------------------------------- 1 | const { readVersionedContents, writeContents } = require("./storage") 2 | 3 | const versionedContents = readVersionedContents() 4 | const dslVersion = versionedContents.version // (This implies: no AST migrations while the server is running.) 5 | let contents = versionedContents.contents 6 | 7 | const express = require("express") 8 | const server = express() 9 | 10 | server.get("/contents", (request, response) => { 11 | response.header("X-DSL-Version", dslVersion) 12 | response.json(contents) 13 | }) 14 | 15 | server.use(express.json({ limit: "1gb" })) 16 | server.put("/contents", (request, response) => { 17 | const newContents = request.body 18 | writeContents(newContents) 19 | contents = newContents 20 | response.send() 21 | }) 22 | 23 | // endpoint to generate src/runtime/index.jsx from contents: 24 | const { generatedIndexJsx } = require("../generator/indexJsx-template") 25 | const { deserialize } = require("../common/ast") 26 | server.get("/contents/indexJsx", (request, response) => { 27 | response.set("Content-Type", "text/plain") 28 | response.send(generatedIndexJsx(deserialize(contents))) 29 | }) 30 | 31 | const { join } = require("path") 32 | server.use(express.static(join(__dirname, "..", "dist"))) 33 | 34 | const port = 8080 35 | server.listen(port, () => { 36 | console.log(`Server started on: http://localhost:${port}/`) 37 | }) 38 | 39 | -------------------------------------------------------------------------------- /ch11/common/file-utils.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require("fs") 2 | 3 | const writeString = (path, data) => { 4 | writeFileSync(path, data) 5 | } 6 | module.exports.writeString = writeString 7 | 8 | 9 | const writeJson = (path, data) => { 10 | writeString(path, JSON.stringify(data, null, 2)) 11 | } 12 | module.exports.writeJson = writeJson 13 | 14 | 15 | const readJson = (path) => 16 | JSON.parse(readFileSync(path, { encoding: "utf8" }).toString()) 17 | module.exports.readJson = readJson 18 | 19 | -------------------------------------------------------------------------------- /ch11/frontend/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch11/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch11/generate-and-run-Runtime.sh: -------------------------------------------------------------------------------- 1 | node generator/generate.js 2 | echo "Wait a couple of seconds for the Runtime to start, before reloading the opened browser tab:" 3 | open http://localhost:8180 4 | npx parcel runtime/index.html --port 8180 --dist-dir dist-runtime 5 | -------------------------------------------------------------------------------- /ch11/generator/generate.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path") 2 | 3 | const { deserialize, isAstObject } = require("../common/ast") 4 | const { writeString } = require("../common/file-utils") 5 | const { readVersionedContents } = require("../backend/storage") 6 | const { issuesFor } = require("../language/constraints") 7 | const { generatedIndexJsx } = require("./indexJsx-template") 8 | 9 | const indexJsxPath = join(__dirname, "..", "runtime", "index.jsx") 10 | 11 | const serializedAst = readVersionedContents().contents 12 | const deserializedAst = deserialize(serializedAst) 13 | 14 | 15 | const printIssue = (issue, astObject) => { 16 | console.log(`[ERROR] on AST object with id='${astObject.id}', concept='${astObject.concept}'; message: "${issue}"`) 17 | } 18 | 19 | const printAllIssuesFor = (astObject, ancestors) => { 20 | if (isAstObject(astObject)) { 21 | issuesFor(astObject, ancestors).forEach((issue) => printIssue(issue, astObject)) 22 | for (const propertyName in astObject.settings) { 23 | printAllIssuesFor(astObject.settings[propertyName], [ astObject, ...ancestors ]) 24 | } 25 | } 26 | if (Array.isArray(astObject)) { 27 | astObject.forEach((item) => printAllIssuesFor(item, ancestors)) 28 | } 29 | } 30 | 31 | printAllIssuesFor(deserializedAst, []) 32 | 33 | 34 | writeString(indexJsxPath, generatedIndexJsx(deserializedAst)) 35 | 36 | -------------------------------------------------------------------------------- /ch11/init/example-AST.js: -------------------------------------------------------------------------------- 1 | const { newAstObject } = require("../common/ast") 2 | const { attributeReferenceTo, binaryOperation, number } = require("../language/factories") 3 | 4 | 5 | const rentalPeriodAttribute = newAstObject("Attribute", { 6 | "name": "rental period", 7 | "type": "date range" 8 | }) 9 | 10 | const rentalPriceBeforeDiscountAttribute = newAstObject("Attribute", { 11 | "name": "rental price before discount", 12 | "type": "amount", 13 | "value": number("0.0"), 14 | "value kind": "initially" 15 | }) 16 | 17 | const discountAttribute = newAstObject("Attribute", { 18 | "name": "discount", 19 | "type": "percentage", 20 | "value": number("0"), 21 | "value kind": "initially" 22 | }) 23 | 24 | const rentalPriceAfterDiscountAttribute = newAstObject("Attribute", { 25 | "name": "rental price after discount", 26 | "type": "amount", 27 | "value": binaryOperation("-", attributeReferenceTo(rentalPriceBeforeDiscountAttribute), binaryOperation("of", attributeReferenceTo(discountAttribute), attributeReferenceTo(rentalPriceBeforeDiscountAttribute))), 28 | "value kind": "computed as" 29 | }) 30 | 31 | 32 | const rental = newAstObject("Record Type", { 33 | "name": "Rental", 34 | "attributes": [ 35 | rentalPeriodAttribute, 36 | rentalPriceBeforeDiscountAttribute, 37 | discountAttribute, 38 | rentalPriceAfterDiscountAttribute 39 | ] 40 | }) 41 | 42 | 43 | module.exports = rental 44 | 45 | -------------------------------------------------------------------------------- /ch11/init/expressions-AST.js: -------------------------------------------------------------------------------- 1 | const { newAstObject } = require("../common/ast") 2 | const { binaryOperation, number, parentheses } = require("../language/factories") 3 | 4 | const exprAsRow = (expr) => newAstObject("Table Row", { items: [ expr ] }) 5 | 6 | const exprAst = newAstObject("Table", { 7 | rows: [ 8 | exprAsRow(binaryOperation("-", number(1), number(2))), 9 | exprAsRow(binaryOperation("of", number(20), number(5))), 10 | exprAsRow(binaryOperation("+", number(1), parentheses(binaryOperation("*", number(2), number(3))))), 11 | exprAsRow(binaryOperation("+", number(1), binaryOperation("*", number(2), number(3)))), 12 | exprAsRow(binaryOperation("+", binaryOperation("*", number(1), number(2)), number(3))), 13 | exprAsRow(binaryOperation("+", number(1), binaryOperation("+", number(2), number(3)))) 14 | ] 15 | }) 16 | 17 | 18 | module.exports = exprAst 19 | 20 | -------------------------------------------------------------------------------- /ch11/init/install-example-DSL-content.js: -------------------------------------------------------------------------------- 1 | const { writeVersionedContents } = require("../backend/storage") 2 | const { serialize } = require("../common/ast") 3 | const rental = require("./example-AST") 4 | 5 | writeVersionedContents(serialize(rental), "v2") 6 | 7 | -------------------------------------------------------------------------------- /ch11/initialize-storage.sh: -------------------------------------------------------------------------------- 1 | rm -f backend/data/*.json # also just delete _all_ JSON contents prior to initialization of the storage 2 | node init/install-example-DSL-content.js 3 | -------------------------------------------------------------------------------- /ch11/language/factories.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Factory functions. 3 | */ 4 | 5 | const { astReferenceTo, newAstObject } = require("../common/ast") 6 | 7 | const attribute = (settings) => newAstObject("Attribute", settings) 8 | module.exports.attribute = attribute 9 | 10 | const attributeReferenceTo = (attribute) => newAstObject("Attribute Reference", { attribute: astReferenceTo(attribute) }) 11 | module.exports.attributeReferenceTo = attributeReferenceTo 12 | 13 | const binaryOperation = (operator, left, right) => newAstObject("Binary Operation", { operator, "left operand": left, "right operand": right }) 14 | module.exports.binaryOperation = binaryOperation 15 | 16 | const number = (value) => newAstObject("Number", { value: "" + value }) 17 | module.exports.number = number 18 | 19 | const parentheses = (expr) => newAstObject("Parentheses", { sub: expr }) 20 | module.exports.parentheses = parentheses 21 | 22 | -------------------------------------------------------------------------------- /ch11/run-Domain-IDE.sh: -------------------------------------------------------------------------------- 1 | node init/migrations.js 2 | echo "Wait a couple of seconds for the Domain IDE to start, before reloading the opened browser tab:" 3 | open http://localhost:8080/ 4 | npx parcel frontend/index.html & 5 | node backend/server.js 6 | -------------------------------------------------------------------------------- /ch11/runtime/components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | import { observer } from "mobx-react" 4 | 5 | import { formatDate } from "./dates" 6 | 7 | 8 | export const FormField = observer(({ label, children }) =>
9 | 10 |
11 | {children} 12 |
13 |
) 14 | 15 | 16 | const convertToType = (newValue /*: string from onChange's event.target.value */, type) => { 17 | switch (type) { 18 | case "number": return Number.parseFloat(newValue) 19 | case "date": return new Date(newValue) 20 | default: return newValue 21 | } 22 | } 23 | 24 | const convertFromType = (value, type) => { 25 | switch (type) { 26 | case "number": return "" + value 27 | case "date": return formatDate(value) 28 | default: return value 29 | } 30 | } 31 | 32 | export const Input = observer(({ type, object, fieldName }) => { 36 | object[fieldName] = convertToType(event.target.value, type) 37 | })} 38 | />) 39 | 40 | /* 41 | * Note: HTML's input element uses the format "yyyy-mm-dd" when type="date" 42 | */ 43 | 44 | -------------------------------------------------------------------------------- /ch11/runtime/dates.js: -------------------------------------------------------------------------------- 1 | const { makeAutoObservable } = require("mobx") 2 | 3 | 4 | const leftPad0 = (num, len) => { 5 | let str = "" + num 6 | if (str.length < len) { 7 | str = "0".repeat(len - str.length) + str 8 | } 9 | return str 10 | } 11 | const formatDate = (date) => `${leftPad0(date.getFullYear(), 4)}-${leftPad0(date.getMonth() + 1, 2)}-${leftPad0(date.getDate(), 2)}` 12 | // date.toString().substring(0, 10) doesn't work... 13 | module.exports.formatDate = formatDate 14 | 15 | 16 | class DateRange { 17 | _from; 18 | _to; 19 | get from() { 20 | return this._from 21 | } 22 | set from(newValue) { 23 | this._from = newValue 24 | if (this._to < this._from) { 25 | this._to = this._from 26 | } 27 | } 28 | get to() { 29 | return this._to 30 | } 31 | set to(newValue) { 32 | this._to = newValue 33 | if (this._from > this._to) { 34 | this._from = this._to 35 | } 36 | } 37 | toString() { 38 | return `${formatDate(this._from)} - ${formatDate(this._to)}` 39 | } 40 | constructor(fromStr, toStr) { 41 | makeAutoObservable(this) 42 | const now = new Date() 43 | this._from = !!fromStr ? new Date(fromStr) : now 44 | this._to = !!toStr ? new Date(toStr) : now 45 | } 46 | } 47 | module.exports.DateRange = DateRange 48 | 49 | -------------------------------------------------------------------------------- /ch11/runtime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch11/runtime/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { makeAutoObservable } from "mobx" 4 | import { observer } from "mobx-react" 5 | 6 | import { FormField, Input } from "./components" 7 | import { DateRange } from "./dates" 8 | 9 | require("./styling.css") 10 | 11 | class Rental { 12 | rentalPeriod = new DateRange() 13 | rentalPriceBeforeDiscount = 0.0 14 | discount = 0 15 | get rentalPriceAfterDiscount() { 16 | return this.rentalPriceBeforeDiscount - this.discount * 0.01 * this.rentalPriceBeforeDiscount 17 | } 18 | constructor() { 19 | makeAutoObservable(this) 20 | } 21 | } 22 | 23 | const RentalForm = observer(({ rental }) =>
24 | 25 | 26 | 27 | 28 | 29 | $ 30 | 31 | 32 | % 33 | 34 | 35 | $ {rental.rentalPriceAfterDiscount.toFixed(2)} 36 | 37 |
) 38 | 39 | const rental = new Rental() 40 | 41 | createRoot(document.getElementById("root")) 42 | .render( 43 | 44 | ) 45 | 46 | -------------------------------------------------------------------------------- /ch11/runtime/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | form { 7 | display: grid; 8 | grid-gap: 0.5em; 9 | } 10 | 11 | div.row { 12 | display: grid; 13 | grid-template-columns: 1fr 1fr; 14 | } 15 | 16 | label:after { 17 | content: ":"; 18 | grid-column: 1 / 2; 19 | } 20 | 21 | div.field { 22 | margin-left: 0.5rem; 23 | grid-column: 2 / 3; 24 | } 25 | 26 | input { 27 | font-size: 18pt; 28 | border: 3px solid yellow; 29 | background-color: lightyellow; 30 | } 31 | 32 | input[type="date"] { 33 | margin-right: 0.3em; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ch12/backend/data/.gitignore: -------------------------------------------------------------------------------- 1 | /contents*.json 2 | /metaData.json 3 | -------------------------------------------------------------------------------- /ch12/backend/server.js: -------------------------------------------------------------------------------- 1 | const { readVersionedContents, writeContents } = require("./storage") 2 | 3 | const versionedContents = readVersionedContents() 4 | const dslVersion = versionedContents.version // (This implies: no AST migrations while the server is running.) 5 | let contents = versionedContents.contents 6 | 7 | const express = require("express") 8 | const server = express() 9 | 10 | server.get("/contents", (request, response) => { 11 | response.header("X-DSL-Version", dslVersion) 12 | response.json(contents) 13 | }) 14 | 15 | server.use(express.json({ limit: "1gb" })) 16 | server.put("/contents", (request, response) => { 17 | const newContents = request.body 18 | writeContents(newContents) 19 | contents = newContents 20 | response.send() 21 | }) 22 | 23 | // endpoint to generate src/runtime/index.jsx from contents: 24 | const { generatedIndexJsx } = require("../generator/indexJsx-template") 25 | const { deserialize } = require("../common/ast") 26 | server.get("/contents/indexJsx", (request, response) => { 27 | response.set("Content-Type", "text/plain") 28 | response.send(generatedIndexJsx(deserialize(contents))) 29 | }) 30 | 31 | const { join } = require("path") 32 | server.use(express.static(join(__dirname, "..", "dist"))) 33 | 34 | const port = 8080 35 | server.listen(port, () => { 36 | console.log(`Server started on: http://localhost:${port}/`) 37 | }) 38 | 39 | -------------------------------------------------------------------------------- /ch12/common/file-utils.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require("fs") 2 | 3 | const writeString = (path, data) => { 4 | writeFileSync(path, data) 5 | } 6 | module.exports.writeString = writeString 7 | 8 | 9 | const writeJson = (path, data) => { 10 | writeString(path, JSON.stringify(data, null, 2)) 11 | } 12 | module.exports.writeJson = writeJson 13 | 14 | 15 | const readJson = (path) => 16 | JSON.parse(readFileSync(path, { encoding: "utf8" }).toString()) 17 | module.exports.readJson = readJson 18 | 19 | -------------------------------------------------------------------------------- /ch12/frontend/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch12/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch12/generate-and-run-Runtime.sh: -------------------------------------------------------------------------------- 1 | node generator/generate.js 2 | echo "Wait a couple of seconds for the Runtime to start, before reloading the opened browser tab:" 3 | open http://localhost:8180 4 | npx parcel runtime/index.html --port 8180 --dist-dir dist-runtime 5 | -------------------------------------------------------------------------------- /ch12/generator/generate.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path") 2 | 3 | const { deserialize, isAstObject } = require("../common/ast") 4 | const { writeString } = require("../common/file-utils") 5 | const { readVersionedContents } = require("../backend/storage") 6 | const { issuesFor } = require("../language/constraints") 7 | const { generatedIndexJsx } = require("./indexJsx-template") 8 | 9 | const indexJsxPath = join(__dirname, "..", "runtime", "index.jsx") 10 | 11 | const serializedAst = readVersionedContents().contents 12 | const deserializedAst = deserialize(serializedAst) 13 | 14 | 15 | const printIssue = (issue, astObject) => { 16 | console.log(`[ERROR] on AST object with id='${astObject.id}', concept='${astObject.concept}'; message: "${issue}"`) 17 | } 18 | 19 | const printAllIssuesFor = (astObject, ancestors) => { 20 | if (isAstObject(astObject)) { 21 | issuesFor(astObject, ancestors).forEach((issue) => printIssue(issue, astObject)) 22 | for (const propertyName in astObject.settings) { 23 | printAllIssuesFor(astObject.settings[propertyName], [ astObject, ...ancestors ]) 24 | } 25 | } 26 | if (Array.isArray(astObject)) { 27 | astObject.forEach((item) => printAllIssuesFor(item, ancestors)) 28 | } 29 | } 30 | 31 | printAllIssuesFor(deserializedAst, []) 32 | 33 | 34 | writeString(indexJsxPath, generatedIndexJsx(deserializedAst)) 35 | 36 | -------------------------------------------------------------------------------- /ch12/init/example-AST.js: -------------------------------------------------------------------------------- 1 | const { newAstObject } = require("../common/ast") 2 | const { attribute, attributeReferenceTo, binaryOperation, number } = require("../language/factories") 3 | 4 | 5 | const rentalPeriodAttribute = attribute({ 6 | "name": "rental period", 7 | "type": "date range" 8 | }) 9 | 10 | const rentalPriceBeforeDiscountAttribute = attribute({ 11 | "name": "rental price before discount", 12 | "type": "amount", 13 | "value": number("0.0"), 14 | "value kind": "initially" 15 | }) 16 | 17 | const discountAttribute = attribute({ 18 | "name": "discount", 19 | "type": "percentage", 20 | "value": number("0"), 21 | "value kind": "initially" 22 | }) 23 | 24 | const rentalPriceAfterDiscountAttribute = attribute({ 25 | "name": "rental price after discount", 26 | "type": "amount", 27 | "value": binaryOperation("-", attributeReferenceTo(rentalPriceBeforeDiscountAttribute), binaryOperation("of", attributeReferenceTo(discountAttribute), attributeReferenceTo(rentalPriceBeforeDiscountAttribute))), 28 | "value kind": "computed as" 29 | }) 30 | 31 | 32 | const rental = newAstObject("Record Type", { 33 | "name": "Rental", 34 | "attributes": [ 35 | rentalPeriodAttribute, 36 | rentalPriceBeforeDiscountAttribute, 37 | discountAttribute, 38 | rentalPriceAfterDiscountAttribute 39 | ] 40 | }) 41 | 42 | 43 | module.exports = rental 44 | 45 | -------------------------------------------------------------------------------- /ch12/init/install-example-DSL-content.js: -------------------------------------------------------------------------------- 1 | const { writeVersionedContents } = require("../backend/storage") 2 | const { serialize } = require("../common/ast") 3 | const rental = require("./example-AST") 4 | 5 | writeVersionedContents(serialize(rental), "v2") 6 | 7 | -------------------------------------------------------------------------------- /ch12/initialize-storage.sh: -------------------------------------------------------------------------------- 1 | rm -f backend/data/*.json # also just delete _all_ JSON contents prior to initialization of the storage 2 | node init/install-example-DSL-content.js 3 | -------------------------------------------------------------------------------- /ch12/language/factories.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Factory functions. 3 | */ 4 | 5 | const { astReferenceTo, newAstObject } = require("../common/ast") 6 | 7 | const attribute = (settings) => newAstObject("Attribute", settings) 8 | module.exports.attribute = attribute 9 | 10 | const attributeReferenceTo = (attribute) => newAstObject("Attribute Reference", { attribute: astReferenceTo(attribute) }) 11 | module.exports.attributeReferenceTo = attributeReferenceTo 12 | 13 | const binaryOperation = (operator, left, right) => newAstObject("Binary Operation", { operator, "left operand": left, "right operand": right }) 14 | module.exports.binaryOperation = binaryOperation 15 | 16 | const number = (value) => newAstObject("Number", { value: "" + value }) 17 | module.exports.number = number 18 | 19 | const parentheses = (expr) => newAstObject("Parentheses", { sub: expr }) 20 | module.exports.parentheses = parentheses 21 | 22 | -------------------------------------------------------------------------------- /ch12/run-Domain-IDE.sh: -------------------------------------------------------------------------------- 1 | node init/migrations.js 2 | echo "Wait a couple of seconds for the Domain IDE to start, before reloading the opened browser tab:" 3 | open http://localhost:8080/ 4 | npx parcel frontend/index.html & 5 | node backend/server.js 6 | -------------------------------------------------------------------------------- /ch12/runtime/components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | import { observer } from "mobx-react" 4 | 5 | import { formatDate } from "./dates" 6 | 7 | 8 | export const FormField = observer(({ label, children }) =>
9 | 10 |
11 | {children} 12 |
13 |
) 14 | 15 | 16 | const convertToType = (newValue /*: string from onChange's event.target.value */, type) => { 17 | switch (type) { 18 | case "number": return Number.parseFloat(newValue) 19 | case "date": return new Date(newValue) 20 | default: return newValue 21 | } 22 | } 23 | 24 | const convertFromType = (value, type) => { 25 | switch (type) { 26 | case "number": return "" + value 27 | case "date": return formatDate(value) 28 | default: return value 29 | } 30 | } 31 | 32 | export const Input = observer(({ type, object, fieldName }) => { 36 | object[fieldName] = convertToType(event.target.value, type) 37 | })} 38 | />) 39 | 40 | /* 41 | * Note: HTML's input element uses the format "yyyy-mm-dd" when type="date" 42 | */ 43 | 44 | -------------------------------------------------------------------------------- /ch12/runtime/dates.js: -------------------------------------------------------------------------------- 1 | const { makeAutoObservable } = require("mobx") 2 | 3 | 4 | const leftPad0 = (num, len) => { 5 | let str = "" + num 6 | if (str.length < len) { 7 | str = "0".repeat(len - str.length) + str 8 | } 9 | return str 10 | } 11 | const formatDate = (date) => `${leftPad0(date.getFullYear(), 4)}-${leftPad0(date.getMonth() + 1, 2)}-${leftPad0(date.getDate(), 2)}` 12 | // date.toString().substring(0, 10) doesn't work... 13 | module.exports.formatDate = formatDate 14 | 15 | 16 | class DateRange { 17 | _from; 18 | _to; 19 | get from() { 20 | return this._from 21 | } 22 | set from(newValue) { 23 | this._from = newValue 24 | if (this._to < this._from) { 25 | this._to = this._from 26 | } 27 | } 28 | get to() { 29 | return this._to 30 | } 31 | set to(newValue) { 32 | this._to = newValue 33 | if (this._from > this._to) { 34 | this._from = this._to 35 | } 36 | } 37 | toString() { 38 | return `${formatDate(this._from)} - ${formatDate(this._to)}` 39 | } 40 | constructor(fromStr, toStr) { 41 | makeAutoObservable(this) 42 | const now = new Date() 43 | this._from = !!fromStr ? new Date(fromStr) : now 44 | this._to = !!toStr ? new Date(toStr) : now 45 | } 46 | } 47 | module.exports.DateRange = DateRange 48 | 49 | -------------------------------------------------------------------------------- /ch12/runtime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch12/runtime/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { makeAutoObservable } from "mobx" 4 | import { observer } from "mobx-react" 5 | 6 | import { FormField, Input } from "./components" 7 | import { DateRange } from "./dates" 8 | 9 | require("./styling.css") 10 | 11 | class Rental { 12 | rentalPeriod = new DateRange() 13 | rentalPriceBeforeDiscount = 0.0 14 | discount = 0 15 | get rentalPriceAfterDiscount() { 16 | return this.rentalPriceBeforeDiscount - this.discount * 0.01 * this.rentalPriceBeforeDiscount 17 | } 18 | constructor() { 19 | makeAutoObservable(this) 20 | } 21 | } 22 | 23 | const RentalForm = observer(({ rental }) =>
24 | 25 | 26 | 27 | 28 | 29 | $ 30 | 31 | 32 | % 33 | 34 | 35 | $ {rental.rentalPriceAfterDiscount.toFixed(2)} 36 | 37 |
) 38 | 39 | const rental = new Rental() 40 | 41 | createRoot(document.getElementById("root")) 42 | .render( 43 | 44 | ) 45 | 46 | -------------------------------------------------------------------------------- /ch12/runtime/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | form { 7 | display: grid; 8 | grid-gap: 0.5em; 9 | } 10 | 11 | div.row { 12 | display: grid; 13 | grid-template-columns: 1fr 1fr; 14 | } 15 | 16 | label:after { 17 | content: ":"; 18 | grid-column: 1 / 2; 19 | } 20 | 21 | div.field { 22 | margin-left: 0.5rem; 23 | grid-column: 2 / 3; 24 | } 25 | 26 | input { 27 | font-size: 18pt; 28 | border: 3px solid yellow; 29 | background-color: lightyellow; 30 | } 31 | 32 | input[type="date"] { 33 | margin-right: 0.3em; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ch13/backend/data/.gitignore: -------------------------------------------------------------------------------- 1 | /contents*.json 2 | /metaData.json 3 | -------------------------------------------------------------------------------- /ch13/backend/server.js: -------------------------------------------------------------------------------- 1 | const { readVersionedContents, writeContents } = require("./storage") 2 | 3 | const versionedContents = readVersionedContents() 4 | const dslVersion = versionedContents.version // (This implies: no AST migrations while the server is running.) 5 | let contents = versionedContents.contents 6 | 7 | const express = require("express") 8 | const server = express() 9 | 10 | server.get("/contents", (request, response) => { 11 | response.header("X-DSL-Version", dslVersion) 12 | response.json(contents) 13 | }) 14 | 15 | server.use(express.json({ limit: "1gb" })) 16 | server.put("/contents", (request, response) => { 17 | const newContents = request.body 18 | writeContents(newContents) 19 | contents = newContents 20 | response.send() 21 | }) 22 | 23 | // endpoint to generate src/runtime/index.jsx from contents: 24 | const { generatedIndexJsx } = require("../generator/indexJsx-template") 25 | const { deserialize } = require("../common/ast") 26 | server.get("/contents/indexJsx", (request, response) => { 27 | response.set("Content-Type", "text/plain") 28 | response.send(generatedIndexJsx(deserialize(contents))) 29 | }) 30 | 31 | const { join } = require("path") 32 | server.use(express.static(join(__dirname, "..", "dist"))) 33 | 34 | const port = 8080 35 | server.listen(port, () => { 36 | console.log(`Server started on: http://localhost:${port}/`) 37 | }) 38 | 39 | -------------------------------------------------------------------------------- /ch13/common/file-utils.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require("fs") 2 | 3 | const writeString = (path, data) => { 4 | writeFileSync(path, data) 5 | } 6 | module.exports.writeString = writeString 7 | 8 | 9 | const writeJson = (path, data) => { 10 | writeString(path, JSON.stringify(data, null, 2)) 11 | } 12 | module.exports.writeJson = writeJson 13 | 14 | 15 | const readJson = (path) => 16 | JSON.parse(readFileSync(path, { encoding: "utf8" }).toString()) 17 | module.exports.readJson = readJson 18 | 19 | -------------------------------------------------------------------------------- /ch13/frontend/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch13/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch13/generate-and-run-Runtime.sh: -------------------------------------------------------------------------------- 1 | node generator/generate.js 2 | echo "Wait a couple of seconds for the Runtime to start, before reloading the opened browser tab:" 3 | open http://localhost:8180 4 | npx parcel runtime/index.html --port 8180 --dist-dir dist-runtime 5 | -------------------------------------------------------------------------------- /ch13/generator/generate.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path") 2 | 3 | const { deserialize, isAstObject } = require("../common/ast") 4 | const { writeString } = require("../common/file-utils") 5 | const { readVersionedContents } = require("../backend/storage") 6 | const { issuesFor } = require("../language/constraints") 7 | const { generatedIndexJsx } = require("./indexJsx-template") 8 | 9 | const indexJsxPath = join(__dirname, "..", "runtime", "index.jsx") 10 | 11 | const serializedAst = readVersionedContents().contents 12 | const deserializedAst = deserialize(serializedAst) 13 | 14 | 15 | const printIssue = (issue, astObject) => { 16 | console.log(`[ERROR] on AST object with id='${astObject.id}', concept='${astObject.concept}'; message: "${issue}"`) 17 | } 18 | 19 | const printAllIssuesFor = (astObject, ancestors) => { 20 | if (isAstObject(astObject)) { 21 | issuesFor(astObject, ancestors).forEach((issue) => printIssue(issue, astObject)) 22 | for (const propertyName in astObject.settings) { 23 | printAllIssuesFor(astObject.settings[propertyName], [ astObject, ...ancestors ]) 24 | } 25 | } 26 | if (Array.isArray(astObject)) { 27 | astObject.forEach((item) => printAllIssuesFor(item, ancestors)) 28 | } 29 | } 30 | 31 | printAllIssuesFor(deserializedAst, []) 32 | 33 | 34 | writeString(indexJsxPath, generatedIndexJsx(deserializedAst)) 35 | 36 | -------------------------------------------------------------------------------- /ch13/init/example-AST.js: -------------------------------------------------------------------------------- 1 | const { newAstObject } = require("../common/ast") 2 | const { attribute, attributeReferenceTo, binaryOperation, number } = require("../language/factories") 3 | 4 | 5 | const rentalPeriodAttribute = attribute({ 6 | "name": "rental period", 7 | "type": "date range" 8 | }) 9 | 10 | const rentalPriceBeforeDiscountAttribute = attribute({ 11 | "name": "rental price before discount", 12 | "type": "amount", 13 | "value": number("0.0"), 14 | "value kind": "initially" 15 | }) 16 | 17 | const discountAttribute = attribute({ 18 | "name": "discount", 19 | "type": "percentage", 20 | "value": number("0"), 21 | "value kind": "initially" 22 | }) 23 | 24 | const rentalPriceAfterDiscountAttribute = attribute({ 25 | "name": "rental price after discount", 26 | "type": "amount", 27 | "value": binaryOperation("-", attributeReferenceTo(rentalPriceBeforeDiscountAttribute), binaryOperation("of", attributeReferenceTo(discountAttribute), attributeReferenceTo(rentalPriceBeforeDiscountAttribute))), 28 | "value kind": "computed as" 29 | }) 30 | 31 | 32 | const rental = newAstObject("Record Type", { 33 | "name": "Rental", 34 | "attributes": [ 35 | rentalPeriodAttribute, 36 | rentalPriceBeforeDiscountAttribute, 37 | discountAttribute, 38 | rentalPriceAfterDiscountAttribute 39 | ] 40 | }) 41 | 42 | 43 | module.exports = rental 44 | 45 | -------------------------------------------------------------------------------- /ch13/init/install-example-DSL-content.js: -------------------------------------------------------------------------------- 1 | const { writeVersionedContents } = require("../backend/storage") 2 | const { serialize } = require("../common/ast") 3 | const rental = require("./example-AST") 4 | 5 | writeVersionedContents(serialize(rental), "v2") 6 | 7 | -------------------------------------------------------------------------------- /ch13/initialize-storage.sh: -------------------------------------------------------------------------------- 1 | rm -f backend/data/*.json # also just delete _all_ JSON contents prior to initialization of the storage 2 | node init/install-example-DSL-content.js 3 | -------------------------------------------------------------------------------- /ch13/language/factories.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Factory functions. 3 | */ 4 | 5 | const { astReferenceTo, newAstObject } = require("../common/ast") 6 | 7 | const attribute = (settings) => newAstObject("Attribute", settings) 8 | module.exports.attribute = attribute 9 | 10 | const attributeReferenceTo = (attribute) => newAstObject("Attribute Reference", { attribute: astReferenceTo(attribute) }) 11 | module.exports.attributeReferenceTo = attributeReferenceTo 12 | 13 | const binaryOperation = (operator, left, right) => newAstObject("Binary Operation", { operator, "left operand": left, "right operand": right }) 14 | module.exports.binaryOperation = binaryOperation 15 | 16 | const number = (value) => newAstObject("Number", { value: "" + value }) 17 | module.exports.number = number 18 | 19 | const parentheses = (expr) => newAstObject("Parentheses", { sub: expr }) 20 | module.exports.parentheses = parentheses 21 | 22 | -------------------------------------------------------------------------------- /ch13/run-Domain-IDE.sh: -------------------------------------------------------------------------------- 1 | node init/migrations.js 2 | echo "Wait a couple of seconds for the Domain IDE to start, before reloading the opened browser tab:" 3 | open http://localhost:8080/ 4 | npx parcel frontend/index.html & 5 | node backend/server.js 6 | -------------------------------------------------------------------------------- /ch13/runtime/components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | import { observer } from "mobx-react" 4 | 5 | import { formatDate } from "./dates" 6 | 7 | 8 | export const FormField = observer(({ label, children }) =>
9 | 10 |
11 | {children} 12 |
13 |
) 14 | 15 | 16 | const convertToType = (newValue /*: string from onChange's event.target.value */, type) => { 17 | switch (type) { 18 | case "number": return Number.parseFloat(newValue) 19 | case "date": return new Date(newValue) 20 | default: return newValue 21 | } 22 | } 23 | 24 | const convertFromType = (value, type) => { 25 | switch (type) { 26 | case "number": return "" + value 27 | case "date": return formatDate(value) 28 | default: return value 29 | } 30 | } 31 | 32 | export const Input = observer(({ type, object, fieldName }) => { 36 | object[fieldName] = convertToType(event.target.value, type) 37 | })} 38 | />) 39 | 40 | /* 41 | * Note: HTML's input element uses the format "yyyy-mm-dd" when type="date" 42 | */ 43 | 44 | -------------------------------------------------------------------------------- /ch13/runtime/dates.js: -------------------------------------------------------------------------------- 1 | const { makeAutoObservable } = require("mobx") 2 | 3 | 4 | const leftPad0 = (num, len) => { 5 | let str = "" + num 6 | if (str.length < len) { 7 | str = "0".repeat(len - str.length) + str 8 | } 9 | return str 10 | } 11 | const formatDate = (date) => `${leftPad0(date.getFullYear(), 4)}-${leftPad0(date.getMonth() + 1, 2)}-${leftPad0(date.getDate(), 2)}` 12 | // date.toString().substring(0, 10) doesn't work... 13 | module.exports.formatDate = formatDate 14 | 15 | 16 | class DateRange { 17 | _from; 18 | _to; 19 | get from() { 20 | return this._from 21 | } 22 | set from(newValue) { 23 | this._from = newValue 24 | if (this._to < this._from) { 25 | this._to = this._from 26 | } 27 | } 28 | get to() { 29 | return this._to 30 | } 31 | set to(newValue) { 32 | this._to = newValue 33 | if (this._from > this._to) { 34 | this._from = this._to 35 | } 36 | } 37 | toString() { 38 | return `${formatDate(this._from)} - ${formatDate(this._to)}` 39 | } 40 | constructor(fromStr, toStr) { 41 | makeAutoObservable(this) 42 | const now = new Date() 43 | this._from = !!fromStr ? new Date(fromStr) : now 44 | this._to = !!toStr ? new Date(toStr) : now 45 | } 46 | } 47 | module.exports.DateRange = DateRange 48 | 49 | -------------------------------------------------------------------------------- /ch13/runtime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch13/runtime/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createRoot } from "react-dom/client" 3 | import { makeAutoObservable } from "mobx" 4 | import { observer } from "mobx-react" 5 | 6 | import { FormField, Input } from "./components" 7 | import { DateRange } from "./dates" 8 | 9 | require("./styling.css") 10 | 11 | class Rental { 12 | rentalPeriod = new DateRange() 13 | rentalPriceBeforeDiscount = 0.0 14 | discount = 0 15 | get rentalPriceAfterDiscount() { 16 | return this.rentalPriceBeforeDiscount - this.discount * 0.01 * this.rentalPriceBeforeDiscount 17 | } 18 | constructor() { 19 | makeAutoObservable(this) 20 | } 21 | } 22 | 23 | const RentalForm = observer(({ rental }) =>
24 | 25 | 26 | 27 | 28 | 29 | $ 30 | 31 | 32 | % 33 | 34 | 35 | $ {rental.rentalPriceAfterDiscount.toFixed(2)} 36 | 37 |
) 38 | 39 | const rental = new Rental() 40 | 41 | createRoot(document.getElementById("root")) 42 | .render( 43 | 44 | ) 45 | 46 | -------------------------------------------------------------------------------- /ch13/runtime/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | form { 7 | display: grid; 8 | grid-gap: 0.5em; 9 | } 10 | 11 | div.row { 12 | display: grid; 13 | grid-template-columns: 1fr 1fr; 14 | } 15 | 16 | label:after { 17 | content: ":"; 18 | grid-column: 1 / 2; 19 | } 20 | 21 | div.field { 22 | margin-left: 0.5rem; 23 | grid-column: 2 / 3; 24 | } 25 | 26 | input { 27 | font-size: 18pt; 28 | border: 3px solid yellow; 29 | background-color: lightyellow; 30 | } 31 | 32 | input[type="date"] { 33 | margin-right: 0.3em; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ch14/backend/data/.gitignore: -------------------------------------------------------------------------------- 1 | /contents*.json 2 | /metaData.json 3 | -------------------------------------------------------------------------------- /ch14/backend/server.js: -------------------------------------------------------------------------------- 1 | const { readVersionedContents, writeContents } = require("./storage") 2 | 3 | const versionedContents = readVersionedContents() 4 | const dslVersion = versionedContents.version // (This implies: no AST migrations while the server is running.) 5 | let contents = versionedContents.contents 6 | 7 | const express = require("express") 8 | const server = express() 9 | 10 | server.get("/contents", (request, response) => { 11 | response.header("X-DSL-Version", dslVersion) 12 | response.json(contents) 13 | }) 14 | 15 | server.use(express.json({ limit: "1gb" })) 16 | server.put("/contents", (request, response) => { 17 | const newContents = request.body 18 | writeContents(newContents) 19 | contents = newContents 20 | response.send() 21 | }) 22 | 23 | // endpoint to generate src/runtime/index.jsx from contents: 24 | const { generatedIndexJsx } = require("../generator/indexJsx-template") 25 | const { deserialize } = require("../common/ast") 26 | server.get("/contents/indexJsx", (request, response) => { 27 | response.set("Content-Type", "text/plain") 28 | response.send(generatedIndexJsx(deserialize(contents))) 29 | }) 30 | 31 | const { join } = require("path") 32 | server.use(express.static(join(__dirname, "..", "dist"))) 33 | 34 | const port = 8080 35 | server.listen(port, () => { 36 | console.log(`Server started on: http://localhost:${port}/`) 37 | }) 38 | 39 | -------------------------------------------------------------------------------- /ch14/common/file-utils.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require("fs") 2 | 3 | const writeString = (path, data) => { 4 | writeFileSync(path, data) 5 | } 6 | module.exports.writeString = writeString 7 | 8 | 9 | const writeJson = (path, data) => { 10 | writeString(path, JSON.stringify(data, null, 2)) 11 | } 12 | module.exports.writeJson = writeJson 13 | 14 | 15 | const readJson = (path) => 16 | JSON.parse(readFileSync(path, { encoding: "utf8" }).toString()) 17 | module.exports.readJson = readJson 18 | 19 | -------------------------------------------------------------------------------- /ch14/frontend/css-util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters the string-typed arguments out, and joins those separated with a space. 3 | * This is convenient for conditional class names, as follows: 4 | * 5 | * asClassNameArgument("foo", false && "none", true && "bar") === "foo bar" 6 | */ 7 | const asClassNameArgument = (...classNames) => classNames.filter((className) => typeof className === "string").join(" ") 8 | module.exports.asClassNameArgument = asClassNameArgument 9 | 10 | -------------------------------------------------------------------------------- /ch14/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Business Rules Management @Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch14/generate-and-run-Runtime.sh: -------------------------------------------------------------------------------- 1 | node generator/generate.js 2 | echo "Wait a couple of seconds for the Runtime to start, before reloading the opened browser tab:" 3 | open http://localhost:8180 4 | npx parcel runtime/index.html --port 8180 --dist-dir dist-runtime 5 | -------------------------------------------------------------------------------- /ch14/generator/generate.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path") 2 | 3 | const { deserialize, isAstObject } = require("../common/ast") 4 | const { writeString } = require("../common/file-utils") 5 | const { readVersionedContents } = require("../backend/storage") 6 | const { issuesFor } = require("../language/constraints") 7 | const { generatedIndexJsx } = require("./indexJsx-template") 8 | 9 | const indexJsxPath = join(__dirname, "..", "runtime", "index.jsx") 10 | 11 | const serializedAst = readVersionedContents().contents 12 | const deserializedAst = deserialize(serializedAst) 13 | 14 | 15 | const printIssue = (issue, astObject) => { 16 | console.log(`[ERROR] on AST object with id='${astObject.id}', concept='${astObject.concept}'; message: "${issue}"`) 17 | } 18 | 19 | const printAllIssuesFor = (astObject, ancestors) => { 20 | if (isAstObject(astObject)) { 21 | issuesFor(astObject, ancestors).forEach((issue) => printIssue(issue, astObject)) 22 | for (const propertyName in astObject.settings) { 23 | printAllIssuesFor(astObject.settings[propertyName], [ astObject, ...ancestors ]) 24 | } 25 | } 26 | if (Array.isArray(astObject)) { 27 | astObject.forEach((item) => printAllIssuesFor(item, ancestors)) 28 | } 29 | } 30 | 31 | printAllIssuesFor(deserializedAst, []) 32 | 33 | 34 | writeString(indexJsxPath, generatedIndexJsx(deserializedAst)) 35 | 36 | -------------------------------------------------------------------------------- /ch14/init/install-example-DSL-content.js: -------------------------------------------------------------------------------- 1 | const { writeVersionedContents } = require("../backend/storage") 2 | const { serialize } = require("../common/ast") 3 | const rental = require("./example-AST") 4 | 5 | writeVersionedContents(serialize(rental), "v2") 6 | 7 | -------------------------------------------------------------------------------- /ch14/initialize-storage.sh: -------------------------------------------------------------------------------- 1 | rm -f backend/data/*.json # also just delete _all_ JSON contents prior to initialization of the storage 2 | node init/install-example-DSL-content.js 3 | -------------------------------------------------------------------------------- /ch14/language/factories.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Factory functions. 3 | */ 4 | 5 | const { astReferenceTo, newAstObject } = require("../common/ast") 6 | 7 | const attribute = (settings) => newAstObject("Attribute", settings) 8 | module.exports.attribute = attribute 9 | 10 | const attributeReferenceTo = (attribute) => newAstObject("Attribute Reference", { attribute: astReferenceTo(attribute) }) 11 | module.exports.attributeReferenceTo = attributeReferenceTo 12 | 13 | const binaryOperation = (operator, left, right) => newAstObject("Binary Operation", { operator, "left operand": left, "right operand": right }) 14 | module.exports.binaryOperation = binaryOperation 15 | 16 | const number = (value) => newAstObject("Number", { value: "" + value }) 17 | module.exports.number = number 18 | 19 | const parentheses = (expr) => newAstObject("Parentheses", { sub: expr }) 20 | module.exports.parentheses = parentheses 21 | 22 | const businessRule = (condition, consequence) => newAstObject("Business Rule", { condition, consequence }) 23 | module.exports.businessRule = businessRule 24 | 25 | const incrementEffect = (value, reffedattribute) => newAstObject("Increment Effect", { value, "attribute reference": attributeReferenceTo(reffedattribute) }) 26 | module.exports.incrementEffect = incrementEffect 27 | 28 | const dateRangeOperation = (operand, operator, timeUnit) => newAstObject("Date Range Operation", { operand, operator, "time unit": timeUnit }) 29 | module.exports.dateRangeOperation = dateRangeOperation 30 | 31 | -------------------------------------------------------------------------------- /ch14/language/time-units.js: -------------------------------------------------------------------------------- 1 | const { weekdays, months } = require("../runtime/dates") 2 | 3 | /** 4 | * Determines whether the given time unit is a weekday('s name). 5 | */ 6 | const isWeekday = (timeUnit) => 7 | weekdays.indexOf(timeUnit) > -1 8 | module.exports.isWeekday = isWeekday 9 | 10 | /** 11 | * Determines whether the given time unit is a month('s name). 12 | */ 13 | const isMonth = (timeUnit) => 14 | months.indexOf(timeUnit) > -1 15 | module.exports.isMonth = isMonth 16 | 17 | /** 18 | * An array of strings containing all time units: 19 | * names of weekdays and months. 20 | */ 21 | const timeUnits = [ ...weekdays, ...months ] 22 | module.exports.timeUnits = timeUnits 23 | 24 | -------------------------------------------------------------------------------- /ch14/run-Domain-IDE.sh: -------------------------------------------------------------------------------- 1 | node init/migrations.js 2 | echo "Wait a couple of seconds for the Domain IDE to start, before reloading the opened browser tab:" 3 | open http://localhost:8080/ 4 | npx parcel frontend/index.html & 5 | node backend/server.js 6 | -------------------------------------------------------------------------------- /ch14/runtime/components.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { action } from "mobx" 3 | import { observer } from "mobx-react" 4 | 5 | import { formatDate } from "./dates" 6 | 7 | 8 | export const FormField = observer(({ label, children }) =>
9 | 10 |
11 | {children} 12 |
13 |
) 14 | 15 | 16 | const convertToType = (newValue /*: string from onChange's event.target.value */, type) => { 17 | switch (type) { 18 | case "number": return Number.parseFloat(newValue) 19 | case "date": return new Date(newValue) 20 | default: return newValue 21 | } 22 | } 23 | 24 | const convertFromType = (value, type) => { 25 | switch (type) { 26 | case "number": return "" + value 27 | case "date": return formatDate(value) 28 | default: return value 29 | } 30 | } 31 | 32 | export const Input = observer(({ type, object, fieldName }) => { 36 | object[fieldName] = convertToType(event.target.value, type) 37 | })} 38 | />) 39 | 40 | /* 41 | * Note: HTML's input element uses the format "yyyy-mm-dd" when type="date" 42 | */ 43 | 44 | -------------------------------------------------------------------------------- /ch14/runtime/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rent-A-Car 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ch14/runtime/styling.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-size: 24pt; 4 | } 5 | 6 | form { 7 | display: grid; 8 | grid-gap: 0.5em; 9 | } 10 | 11 | div.row { 12 | display: grid; 13 | grid-template-columns: 1fr 1fr; 14 | } 15 | 16 | label:after { 17 | content: ":"; 18 | grid-column: 1 / 2; 19 | } 20 | 21 | div.field { 22 | margin-left: 0.5rem; 23 | grid-column: 2 / 3; 24 | } 25 | 26 | input { 27 | font-size: 18pt; 28 | border: 3px solid yellow; 29 | background-color: lightyellow; 30 | } 31 | 32 | input[type="date"] { 33 | margin-right: 0.3em; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /ch15/README.md: -------------------------------------------------------------------------------- 1 | # Code from chapter 15 2 | 3 | Organized per book section: 4 | 5 | * [Parsing, and textual DSLs.](./textual) 6 | * [Listing 15.1. A purely textual variant of the “Rental” record type example DSL content (without business rules).](./textual/Rental.txt) 7 | * [Listing 15.2. An ANTLR grammar that is able to parse Listing 15.1.](./textual/recordType.g4) 8 | 9 | * [Using terms with the prefix “meta”.](./metamodel) 10 | * [Example execution (partial) of Exercise 15.5.](metamodel/concepts.js) 11 | 12 | -------------------------------------------------------------------------------- /ch15/meta-model/concepts.js: -------------------------------------------------------------------------------- 1 | const Concepts = { 2 | attribute: "Attribute", 3 | attributeRef: "Attribute Reference", 4 | number: "Number", 5 | recordType: "Record Type" 6 | } 7 | module.exports.Concepts = Concepts 8 | 9 | -------------------------------------------------------------------------------- /ch15/metamodel/concepts.js: -------------------------------------------------------------------------------- 1 | const Concepts = { 2 | attribute: "Attribute", 3 | attributeRef: "Attribute Reference", 4 | number: "Number", 5 | recordType: "Record Type" 6 | } 7 | module.exports.Concepts = Concepts 8 | 9 | -------------------------------------------------------------------------------- /ch15/textual/Rental.txt: -------------------------------------------------------------------------------- 1 | record type "Rental" having attributes: 2 | "rental period": date range 3 | "rental price before discount": amount initially 0.0 4 | "discount": percentage initially 0 5 | "rental price after discount": amount initially -> "rental price before discount" 6 | -------------------------------------------------------------------------------- /ch15/textual/recordType.g4: -------------------------------------------------------------------------------- 1 | grammar recordType; 2 | 3 | STRING: '"' [A-Za-z0-9 ]+ '"' ; 4 | 5 | type: 'amount' | 'date range' | 'percentage' ; 6 | 7 | WS: (' ' | '\t' | '\n')+ ; 8 | 9 | attributeReference: '->' WS? STRING ; 10 | 11 | fragment DIGIT: [0-9] ; 12 | 13 | number: DIGIT+ ('.' DIGIT+)? ; 14 | 15 | value: attributeReference | number ; 16 | 17 | attribute: STRING WS? ':' WS? type (WS 'initially' WS? value)? ; 18 | 19 | recordType: WS? 'record type' WS? STRING WS? 'having attributes:' (WS? attribute)* WS? ; 20 | 21 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": false 4 | }, 5 | "exclude": ["node_modules"] 6 | } 7 | -------------------------------------------------------------------------------- /mps/.gitattributes: -------------------------------------------------------------------------------- 1 | trace.info text merge=mps 2 | generated text merge=mps 3 | dependencies text merge=mps 4 | *.mpl text merge=mps 5 | *.msd text merge=mps 6 | *.devkit text merge=mps 7 | *.mpr text merge=mps 8 | *.mpsr text merge=mps 9 | *.model text merge=mps 10 | *.mps text merge=mps 11 | -------------------------------------------------------------------------------- /mps/.gitignore: -------------------------------------------------------------------------------- 1 | /.mps/workspace.xml 2 | classes_gen/ 3 | source_gen/ 4 | source_gen.caches/ 5 | 6 | -------------------------------------------------------------------------------- /mps/.mps/.name: -------------------------------------------------------------------------------- 1 | ExampleDSLWithMps -------------------------------------------------------------------------------- /mps/.mps/migration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /mps/.mps/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mps/.mps/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /mps/languages/BusinessDsl/generator/templates/BusinessDsl.generator.templates@generator.mps: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /mps/solutions/CarRentalCompany/CarRentalCompany.msd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Building-User-Friendly-DSLs-code", 3 | "version": "0.11.0-dev", 4 | "description": "All the code from the book “Building User-Friendly DSLs” by Meinte Boersma for Manning Publications.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/dslmeinte/Building-User-Friendly-DSLs-code.git" 8 | }, 9 | "author": "Meinte Boersma (for Manning Publications)", 10 | "bugs": { 11 | "url": "https://github.com/dslmeinte/Building-User-Friendly-DSLs-code/issues" 12 | }, 13 | "homepage": "https://www.manning.com/books/building-user-friendly-dsls", 14 | "dependencies": { 15 | "express": "^4.18.2", 16 | "mobx": "^6.8.0", 17 | "mobx-react": "^7.6.0", 18 | "nanoid": "^3.3.4", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0" 21 | }, 22 | "devDependencies": { 23 | "parcel": "^2.8.3", 24 | "process": "^0.11.10" 25 | } 26 | } 27 | --------------------------------------------------------------------------------