├── .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 | ?
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 | ?
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 }) => )
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 | ?
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 }) => )
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 }) => )
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 }) => )
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 }) => )
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 }) => )
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 |
--------------------------------------------------------------------------------