Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 19124b6

Browse filesBrowse files
authored
Feature rule error arguments (#471)
1 parent 7c4425c commit 19124b6
Copy full SHA for 19124b6

File tree

Expand file treeCollapse file tree

41 files changed

+514
-110
lines changed
Filter options

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Dismiss banner
Expand file treeCollapse file tree

41 files changed

+514
-110
lines changed

‎.github/workflows/ci.yml

Copy file name to clipboardExpand all lines: .github/workflows/ci.yml
+1-1Lines changed: 1 addition & 1 deletion
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

Copy file name to clipboardExpand all lines: cli/package.json
+3-3Lines changed: 3 additions & 3 deletions
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

Copy file name to clipboardExpand all lines: cli/yarn.lock
+4-4Lines changed: 4 additions & 4 deletions
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

Copy file name to clipboardExpand all lines: documentation/docs/reference/rule.md
+64Lines changed: 64 additions & 0 deletions
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.
+1-1Lines changed: 1 addition & 1 deletion
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
}
+2-2Lines changed: 2 additions & 2 deletions
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
}
+2-2Lines changed: 2 additions & 2 deletions
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
}
+3-3Lines changed: 3 additions & 3 deletions
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

Copy file name to clipboardExpand all lines: transpiler/__tests__/ast/core/builders/domainRuleBuilder.ts
+6-2Lines changed: 6 additions & 2 deletions
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

Copy file name to clipboardExpand all lines: transpiler/__tests__/ast/core/mocks/domainRule.ts
+35Lines changed: 35 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: transpiler/__tests__/end-to-end/mocks/entity-primitives/arrays/array-of-vo.output.mock.ts
+1-1Lines changed: 1 addition & 1 deletion
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

Copy file name to clipboardExpand all lines: transpiler/__tests__/end-to-end/mocks/entity-primitives/index.ts
+9Lines changed: 9 additions & 0 deletions
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
];
+38Lines changed: 38 additions & 0 deletions
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+
}
+48Lines changed: 48 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: transpiler/__tests__/end-to-end/mocks/entity-primitives/regular-vo-s/output.mock.ts
+1-1Lines changed: 1 addition & 1 deletion
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.