Skip to content

Commit 19124b6

Browse files
authored
Feature rule error arguments (#471)
1 parent 7c4425c commit 19124b6

File tree

41 files changed

+514
-110
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+514
-110
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,4 @@ jobs:
5151
working-directory: ./transpiler
5252
env:
5353
NODE_OPTIONS: '--max_old_space_size=4096'
54-
run: npm run test-silent
54+
run: yarn test:swc --silent

cli/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bitloops/bitloops-language-cli",
3-
"version": "0.4.2",
3+
"version": "0.4.3",
44
"description": "Bitloops Language CLI",
55
"type": "module",
66
"engines": {
@@ -83,7 +83,7 @@
8383
"author": "Bitloops S.A.",
8484
"license": "GPL-3.0",
8585
"dependencies": {
86-
"@bitloops/bl-transpiler": "^0.6.5",
86+
"@bitloops/bl-transpiler": "^0.6.11",
8787
"axios": "^1.1.3",
8888
"chalk": "5.0.1",
8989
"cli-progress": "^3.12.0",
@@ -100,4 +100,4 @@
100100
"volta": {
101101
"node": "16.13.0"
102102
}
103-
}
103+
}

cli/yarn.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -290,10 +290,10 @@
290290
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
291291
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
292292

293-
"@bitloops/bl-transpiler@^0.6.5":
294-
version "0.6.5"
295-
resolved "https://registry.yarnpkg.com/@bitloops/bl-transpiler/-/bl-transpiler-0.6.5.tgz#d49fdb72bf38349125d44f991b2c82b45ce210df"
296-
integrity sha512-Z3QLLJL79HwPPrkYfy7UcxxeRMBtFAmA3FKRdvqaAz3HZRURpKwwkm5B0F2SMSFGxGossTGL4VitpGbKCtsK7A==
293+
"@bitloops/bl-transpiler@^0.6.11":
294+
version "0.6.11"
295+
resolved "https://registry.yarnpkg.com/@bitloops/bl-transpiler/-/bl-transpiler-0.6.11.tgz#6be3bf6bab6a7915539708a8b18c05d573417227"
296+
integrity sha512-rE39fr4cupvIrkefxY3G0aZQ6eugZK0DXSLL8BOc6vHXNemCJ1Hi1+eDUqurlcv4Wh+AfzCjlaT6F/Xg0M/U2A==
297297
dependencies:
298298
antlr4 "4.11.0"
299299
lodash "^4.17.21"

documentation/docs/reference/rule.md

+64
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,67 @@ keywords:
2525
# Rule
2626

2727
A Rule or a Business Rule is a language element which captures some business rules of the language. They are used inside the domain layer to enforce the invariants.
28+
It provides a structured way to encapsulate complex business conditions and exceptions, ensuring they are consistently enforced across the domain layer of your application.
29+
30+
## Syntax
31+
32+
Each Rule can throw a Domain Error when its associated condition is not satisfied. The Domain Error serves as an exception mechanism indicating a violation of business rules.
33+
34+
For instance, let's consider the following Domain Error:
35+
36+
```typescript
37+
DomainError InsufficientBalanceError(balance: float) {
38+
message: `Insufficient balance: ${balance} in account`,
39+
errorId: 'INSUFFICIENT_BALANCE`
40+
}
41+
```
42+
43+
This error is thrown when an operation attempts to reduce an account's balance below zero.
44+
45+
We define the corresponding Domain Rule, AccountCannotHaveNegativeBalanceRule, as shown below:
46+
47+
We would declare the corresponding Domain Rule like this:
48+
49+
```typescript
50+
Rule AccountCannotHaveNegativeBalanceRule(amount: float, balance: float) throws DomainErrors.InsufficientBalanceError {
51+
const balanceUpdated = balance - amount;
52+
isBrokenIf(balanceUpdated < 0, (balanceUpdated, amount));
53+
}
54+
```
55+
56+
Here, the `AccountCannotHaveNegativeBalanceRule` Rule encapsulates the invariant that an account's balance cannot become negative. If the amount to be subtracted from the balance would result in a negative value, the `isBrokenIf` function throws the `InsufficientBalanceError` Domain Error.
57+
58+
### Using isBrokenIf
59+
60+
The `isBrokenIf` function is central to a Domain Rule definition. This function accepts two arguments:
61+
62+
1. A boolean condition to be evaluated.
63+
2. The arguments to be passed to the Domain Error, in case the condition is evaluated to `true`.
64+
65+
The second argument of `isBrokenIf` maps directly to the parameters of the Domain Error. In the `AccountCannotHaveNegativeBalanceRule` example above, `(balanceUpdated, amount)` are passed as the second argument, which are then utilized by the `InsufficientBalanceError`.
66+
67+
```typescript
68+
isBrokenIf(balanceUpdated < 0, (balanceUpdated, amount));
69+
```
70+
71+
In situations where the Domain Error does not expect any arguments, you can omit the second argument from `isBrokenIf`.
72+
73+
For example, if we have a `GenericError` that doesn't expect any arguments:
74+
75+
```typescript
76+
DomainError GenericError() {
77+
message: `A generic error has occurred`,
78+
errorId: 'GENERIC_ERROR`
79+
}
80+
```
81+
82+
We could write a corresponding rule like this:
83+
84+
```typescript
85+
Rule SomeGenericRule(parameter: string) throws DomainErrors.GenericError {
86+
const someCondition = /_ evaluate some condition _/
87+
isBrokenIf(someCondition);
88+
}
89+
```
90+
91+
Here, the `isBrokenIf` function only requires the condition to be evaluated because `GenericError` doesn't take any additional arguments.
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
Rule ValidEmailRule(email: string) throws DomainErrors.InvalidEmailError {
22
const re = /\S+@\S+\.\S+/;
3-
isBrokenIf(re.test(email) == false);
3+
isBrokenIf(re.test(email) == false, (email));
44
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
Rule CompletedTodosIsPositiveNumberRule(counter: int32) throws DomainErrors.InvalidTodosCounterError {
2-
isBrokenIf(counter < 0);
2+
isBrokenIf(counter < 0, (counter));
33
}
44

55
Rule ValidEmailRule(email: string) throws DomainErrors.InvalidEmailDomainError {
66
const re = /\S+@\S+\.\S+/;
7-
isBrokenIf(re.test(email) == false);
7+
isBrokenIf(re.test(email) == false, (email));
88
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
DomainError TodoAlreadyCompletedError(completed: bool, id: string) {
1+
DomainError TodoAlreadyCompletedError(id: string) {
22
message: `Todo ${id} is already completed`,
33
errorId: 'e09ec42c-4d31-4f7c-b68a-b68a78-b68a655'
44
}
@@ -8,7 +8,7 @@ DomainError TitleOutOfBoundsError(title: string) {
88
errorId: 'a12ec42c-4d31-4f7c-b68a-b68a78-b68a655'
99
}
1010

11-
DomainError TodoAlreadyUncompletedError(completed: bool, id: string) {
11+
DomainError TodoAlreadyUncompletedError(id: string) {
1212
message: `Todo ${id} is already uncompleted`,
1313
errorId: '24225fc3-9137-4f2f-a35d-b86f6d4ad68e'
1414
}
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
Rule TitleOutOfBoundsRule(title: string) throws DomainErrors.TitleOutOfBoundsError {
2-
isBrokenIf(title.length > 150 OR title.length < 4);
2+
isBrokenIf(title.length > 150 OR title.length < 4, (title));
33
}
44

55
Rule TodoAlreadyCompletedRule(completed: bool, todoId: string) throws DomainErrors.TodoAlreadyCompletedError {
6-
isBrokenIf(completed);
6+
isBrokenIf(completed, (todoId));
77
}
88

99
Rule TodoAlreadyUncompletedRule(completed: bool, todoId: string) throws DomainErrors.TodoAlreadyUncompletedError {
10-
isBrokenIf(NOT completed);
10+
isBrokenIf(NOT completed, (todoId));
1111
}

transpiler/__tests__/ast/core/builders/domainRuleBuilder.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
TStatements,
66
TExpression,
77
TCondition,
8+
TArgumentList,
89
} from '../../../../src/types.js';
910

1011
export class DomainRuleBuilder implements IBuilder<TDomainRule> {
@@ -34,8 +35,11 @@ export class DomainRuleBuilder implements IBuilder<TDomainRule> {
3435
return this;
3536
}
3637

37-
public withIsBrokenIfCondition(condition: TExpression): DomainRuleBuilder {
38-
this.isBrokenIfCondition = { condition };
38+
public withIsBrokenIfCondition(
39+
condition: TExpression,
40+
errorArguments?: TArgumentList,
41+
): DomainRuleBuilder {
42+
this.isBrokenIfCondition = { condition, ...errorArguments };
3943
return this;
4044
}
4145

transpiler/__tests__/ast/core/mocks/domainRule.ts

+35
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { DomainRuleBuilder } from '../builders/domainRuleBuilder.js';
2222
import { ParameterListBuilderDirector } from '../builders/parameterListBuilderDirector.js';
2323
import { ExpressionBuilderDirector } from '../builders/expressionDirector.js';
2424
import { ConstDeclarationBuilderDirector } from '../builders/statement/variableDeclarationDirector.js';
25+
import { ArgumentListBuilderDirector } from '../builders/argumentListBuilderDirector.js';
2526

2627
type DomainRuleDeclarationTestCase = {
2728
description: string;
@@ -91,4 +92,38 @@ export const validDomainRuleStatementTestCases: DomainRuleDeclarationTestCase[]
9192
])
9293
.build(),
9394
},
95+
{
96+
description: 'Domain rule with statement and different error arguments',
97+
fileId: 'testFile.bl',
98+
inputBLString: `Rule IsValidTitleRule(title: string) throws DomainErrors.InvalidTitleError {
99+
const titleLength = title.length;
100+
isBrokenIf (titleLength > 150 OR titleLength < 4, (titleLength, title));
101+
}`,
102+
domainRuleDeclaration: new DomainRuleBuilder()
103+
.withIdentifier('IsValidTitleRule')
104+
.withParameters(new ParameterListBuilderDirector().buildStringParams('title'))
105+
.withThrowsError('DomainErrors.InvalidTitleError')
106+
.withIsBrokenIfCondition(
107+
new ExpressionBuilderDirector().buildLogicalOrExpression(
108+
new ExpressionBuilderDirector().buildRelationalExpression(
109+
new ExpressionBuilderDirector().buildIdentifierExpression('titleLength'),
110+
new ExpressionBuilderDirector().buildInt32LiteralExpression(150),
111+
'>',
112+
),
113+
new ExpressionBuilderDirector().buildRelationalExpression(
114+
new ExpressionBuilderDirector().buildIdentifierExpression('titleLength'),
115+
new ExpressionBuilderDirector().buildInt32LiteralExpression(4),
116+
'<',
117+
),
118+
),
119+
new ArgumentListBuilderDirector().buildArgumentList(['titleLength', 'title']),
120+
)
121+
.withBodyStatements([
122+
new ConstDeclarationBuilderDirector().buildConstDeclarationWithMemberDotExpression({
123+
name: 'titleLength',
124+
rightMembers: ['title', 'length'],
125+
}),
126+
])
127+
.build(),
128+
},
94129
];

transpiler/__tests__/end-to-end/mocks/entity-primitives/arrays/array-of-vo.output.mock.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import { StatusVO, TStatusVOPrimitives } from './status.value-object';
88
import { RowEntity, TRowEntityPrimitives } from './row.entity';
99
export type TDocumentEntityPrimitives = {
10-
id: string;
10+
id?: string;
1111
name: string;
1212
locations: TDocumentLocationVOPrimitives[];
1313
rows: TRowEntityPrimitives[];

transpiler/__tests__/end-to-end/mocks/entity-primitives/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,13 @@ export const ENTITY_PRIMITIVES_END_TO_END_TEST_CASES = [
1919
'transpiler/__tests__/end-to-end/mocks/entity-primitives/regular-vo-s/output.mock.ts',
2020
),
2121
},
22+
{
23+
description: 'Test optional properties',
24+
input: FileUtil.readFileString(
25+
'transpiler/__tests__/end-to-end/mocks/entity-primitives/optional-properties/input.bl',
26+
),
27+
output: FileUtil.readFileString(
28+
'transpiler/__tests__/end-to-end/mocks/entity-primitives/optional-properties/output.mock.ts',
29+
),
30+
},
2231
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
Props MoneyProps {
2+
string currency;
3+
AmountVO amount;
4+
}
5+
6+
ValueObject MoneyVO {
7+
static create(props: MoneyProps): (OK(MoneyVO), Errors()) {
8+
}
9+
}
10+
11+
Props AmountProps {
12+
int32 value;
13+
}
14+
ValueObject AmountVO {
15+
static create(props: AmountProps): (OK(AmountVO), Errors()) {
16+
}
17+
}
18+
19+
Props AccountProps {
20+
optional UUIDv4 id;
21+
optional MoneyVO price;
22+
optional RowEntity row;
23+
optional RowEntity[] rows;
24+
}
25+
26+
Root Entity AccountEntity {
27+
static create(props: AccountProps): (OK(AccountEntity), Errors()) { }
28+
}
29+
30+
// **** Row Entity ****
31+
Props RowProps {
32+
optional UUIDv4 id;
33+
string description;
34+
}
35+
36+
Entity RowEntity {
37+
static create(props: RowProps): (OK(RowEntity), Errors()) { }
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Domain, Either, ok } from '@bitloops/bl-boilerplate-core';
2+
import { AccountProps } from './account.props';
3+
import { MoneyVO, TMoneyVOPrimitives } from './money.value-object';
4+
import { RowEntity, TRowEntityPrimitives } from './row.entity';
5+
import { AmountVO } from './amount.value-object';
6+
export type TAccountEntityPrimitives = {
7+
id?: string;
8+
price?: TMoneyVOPrimitives;
9+
row?: TRowEntityPrimitives;
10+
rows?: TRowEntityPrimitives[];
11+
};
12+
export class AccountEntity extends Domain.Aggregate<AccountProps> {
13+
private constructor(props: AccountProps) {
14+
super(props, props.id);
15+
}
16+
public static create(props: AccountProps): Either<AccountEntity, never> {
17+
return ok(new AccountEntity(props));
18+
}
19+
get id() {
20+
return this._id;
21+
}
22+
get price(): MoneyVO {
23+
return this.props.price;
24+
}
25+
get row(): RowEntity {
26+
return this.props.row;
27+
}
28+
get rows(): RowEntity[] {
29+
return this.props.rows;
30+
}
31+
public static fromPrimitives(data: TAccountEntityPrimitives): AccountEntity {
32+
const AccountEntityProps = {
33+
id: new Domain.UUIDv4(data.id) as Domain.UUIDv4,
34+
price: data.price ? MoneyVO.fromPrimitives(data.price) : undefined,
35+
row: data.row ? RowEntity.fromPrimitives(data.row) : undefined,
36+
rows: data.rows ? data.rows.map((x) => RowEntity.fromPrimitives(x)) : undefined,
37+
};
38+
return new AccountEntity(AccountEntityProps);
39+
}
40+
public toPrimitives(): TAccountEntityPrimitives {
41+
return {
42+
id: this.id.toString(),
43+
price: this.price?.toPrimitives(),
44+
row: this.row?.toPrimitives(),
45+
rows: this.rows?.map((x) => x.toPrimitives()),
46+
};
47+
}
48+
}

transpiler/__tests__/end-to-end/mocks/entity-primitives/regular-vo-s/output.mock.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { MoneyVO, TMoneyVOPrimitives } from './money.value-object';
44
import { RowEntity, TRowEntityPrimitives } from './row.entity';
55
import { AmountVO } from './amount.value-object';
66
export type TAccountEntityPrimitives = {
7-
id: string;
7+
id?: string;
88
price: TMoneyVOPrimitives;
99
row: TRowEntityPrimitives;
1010
};

0 commit comments

Comments
 (0)