Skip to content

Commit 8bc82ae

Browse files
Merge remote-tracking branch 'master/develop'
# Conflicts: # .github/workflows/on-push.yml
2 parents 7f56f34 + e779d23 commit 8bc82ae

35 files changed

Lines changed: 1491 additions & 206 deletions

File tree

.github/workflows/on-push.yml

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,105 @@ name: CI
22

33
on:
44
push:
5-
branches: [develop]
5+
branches: [ develop, master ]
66
pull_request:
7-
branches: [develop]
7+
branches: [ develop, master ]
8+
9+
permissions:
10+
contents: read
11+
pull-requests: write
12+
checks: write
13+
security-events: write
814

915
jobs:
10-
quality:
16+
17+
build:
1118
runs-on: ubuntu-latest
1219

13-
permissions:
14-
contents: read
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- uses: actions/setup-java@v5
24+
with:
25+
distribution: temurin
26+
java-version: 25
27+
cache: maven
28+
29+
- name: Compile
30+
run: ./mvnw -B -ntp -DskipTests compile
31+
32+
33+
test:
34+
runs-on: ubuntu-latest
35+
needs: build
1536

1637
steps:
17-
- name: Checkout
18-
uses: actions/checkout@v4
38+
- uses: actions/checkout@v4
39+
40+
- uses: actions/setup-java@v5
41+
with:
42+
distribution: temurin
43+
java-version: 25
44+
cache: maven
45+
46+
- name: Run tests
47+
run: ./mvnw -B test
48+
49+
- name: Publish Test Report
50+
uses: mikepenz/action-junit-report@v4
51+
if: always()
1952
with:
20-
fetch-depth: 0
53+
report_paths: '**/target/surefire-reports/*.xml'
54+
55+
analyze:
56+
name: CodeQL Analysis
57+
runs-on: ubuntu-latest
58+
59+
strategy:
60+
fail-fast: false
61+
matrix:
62+
language: [ 'java' ]
63+
64+
steps:
65+
- name: Checkout repository
66+
uses: actions/checkout@v4
2167

2268
- name: Setup Java
2369
uses: actions/setup-java@v5
2470
with:
2571
distribution: temurin
26-
java-version: "25"
72+
java-version: 25
2773
cache: maven
2874

29-
- name: Build and test
30-
run: ./mvnw -B verify
75+
- name: Initialize CodeQL
76+
uses: github/codeql-action/init@v3
77+
with:
78+
languages: java
79+
80+
- name: Build project
81+
run: ./mvnw -B -DskipTests compile
82+
83+
- name: Perform CodeQL analysis
84+
uses: github/codeql-action/analyze@v3
3185

32-
- name: Sonar analysis
33-
run: ./mvnw -B sonar:sonar
86+
sonar:
87+
if: github.ref == 'refs/heads/master'
88+
runs-on: ubuntu-latest
89+
needs: test
90+
91+
steps:
92+
- uses: actions/checkout@v4
93+
94+
- uses: actions/setup-java@v5
95+
with:
96+
distribution: temurin
97+
java-version: 25
98+
cache: maven
99+
100+
- name: Build + Sonar analysis
101+
run: |
102+
./mvnw -B verify sonar:sonar \
103+
-Dsonar.host.url=https://sonarcloud.io
34104
env:
35105
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
106+
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
@startuml
2+
title error-utils: exception workflow (current core + spring-webmvc split)
3+
4+
autonumber
5+
hide footbox
6+
7+
actor Client
8+
participant "Controller / Service" as App
9+
participant "GlobalApiExceptionHandler" as Handler
10+
participant "ApiErrorFactory\n(Spring facade)" as PublicFactory
11+
participant "WebApiErrorFactory" as WebFactory
12+
participant "TraceContextResolver" as TraceResolver
13+
participant "ErrorMetadataSanitizer" as Sanitizer
14+
participant "ApiErrorAssembler\n(core)" as Assembler
15+
participant "ErrorResponseCustomizer*" as Customizers
16+
17+
note over PublicFactory, Assembler
18+
Current split:
19+
- spring-webmvc: exception mapping, request adaptation,
20+
trace resolution, sanitizer, customizers
21+
- core: ApiError model, ApiErrorInput, ApiErrorAssembler,
22+
BusinessException / InternalException / ErrorCode
23+
end note
24+
25+
Client -> App: HTTP request
26+
App -> App: execute business logic
27+
28+
alt Business exception
29+
App --> Handler: throw BusinessException\n(or NotFoundException /\nConflictException /\nPolicyViolationException)
30+
Handler -> PublicFactory: fromBusinessException(ex, request)
31+
PublicFactory -> WebFactory: inherited implementation
32+
WebFactory -> WebFactory: ex.errorCode()\nex.getMessage()\nex.violations()\nex.metadata()
33+
else Internal exception
34+
App --> Handler: throw InternalException
35+
Handler -> Handler: log.error(...)
36+
Handler -> PublicFactory: fromInternalException(ex, request)
37+
PublicFactory -> WebFactory: inherited implementation
38+
WebFactory -> WebFactory: ex.errorCode()\nsafeTechnicalMessage(...)\nviolations=[]\nmetadata={}
39+
else Unhandled exception
40+
App --> Handler: throw Throwable
41+
Handler -> Handler: log.error(...)
42+
Handler -> PublicFactory: fromUnhandled(ex, request)
43+
PublicFactory -> WebFactory: inherited implementation
44+
WebFactory -> WebFactory: StandardErrorCode.TECHNICAL_ERROR\nsafeTechnicalMessage(...)\nviolations=[]\nmetadata={}
45+
end
46+
47+
group Web factory responsibilities
48+
WebFactory -> WebFactory: normalize violations
49+
50+
WebFactory -> TraceResolver: resolve(request)
51+
note right of TraceResolver
52+
Lookup order:
53+
1. request attribute
54+
2. header
55+
3. MDC
56+
end note
57+
TraceResolver --> WebFactory: TraceContext(correlationId, traceId)
58+
59+
WebFactory -> Sanitizer: sanitize(metadata)
60+
Sanitizer --> WebFactory: sanitized metadata
61+
62+
WebFactory -> WebFactory: create ApiErrorInput(\nInstant.now(clock), errorCode,\nmessage, request URI,\noptional correlationId,\noptional traceId,\nviolations, metadata)
63+
note right of WebFactory
64+
ids are included only when enabled
65+
in ErrorUtilsProperties
66+
end note
67+
end
68+
69+
group Core assembly
70+
WebFactory -> Assembler: assemble(ApiErrorInput)
71+
Assembler -> Assembler: resolve HTTP status name
72+
Assembler --> WebFactory: ApiError
73+
end
74+
75+
group Web post-processing
76+
WebFactory -> WebFactory: build ErrorResponseContext
77+
loop for each customizer
78+
WebFactory -> Customizers: customize(currentApiError, context)
79+
alt customizer returned replacement
80+
Customizers --> WebFactory: ApiError
81+
else customizer returned null
82+
Customizers --> WebFactory: keep current payload
83+
end
84+
end
85+
end
86+
87+
WebFactory --> PublicFactory: final ApiError
88+
PublicFactory --> Handler: final ApiError
89+
Handler --> Client: ResponseEntity<ApiError>\nstatus = body.status()
90+
91+
note over Client
92+
Validation-specific flow is omitted here.
93+
This diagram covers exception rendering only.
94+
end note
95+
96+
@enduml
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
@startuml
2+
title error-utils: validation framework workflow
3+
4+
autonumber
5+
hide footbox
6+
7+
actor Caller
8+
participant "Validator" as Entry
9+
participant "ObjectValidator<T>" as ObjValidator
10+
participant "ValidationCollector" as Collector
11+
participant "ValidationChain" as Chain
12+
participant "ValidationStep<T>\nor inline lambda" as Step
13+
participant "ValidationFailure" as Failure
14+
participant "ValidationResult" as Result
15+
participant "BusinessException\n(default)" as BusinessEx
16+
17+
note over Entry, Chain
18+
Validation module is Spring-free.
19+
Default start mode = COLLECT_ALL.
20+
end note
21+
22+
Caller -> Entry: forObject(target)
23+
Entry -> Chain: start()
24+
Chain --> Entry: new chain(mode=COLLECT_ALL, failures=[])
25+
Entry -> ObjValidator: new ObjectValidator(target, chain)
26+
ObjValidator -> Collector: new ValidationCollector(chain)
27+
Entry --> Caller: ObjectValidator<T>
28+
29+
alt switch to fail-fast
30+
Caller -> ObjValidator: failFast()
31+
ObjValidator -> Chain: failFast()
32+
Chain --> ObjValidator: new chain view(mode=FAIL_FAST,\nshared failures list)
33+
ObjValidator --> Caller: new ObjectValidator(target, chainView)
34+
else keep/default collect-all
35+
Caller -> ObjValidator: collectAll() or keep default
36+
ObjValidator -> Chain: collectAll()
37+
Chain --> ObjValidator: new chain view(mode=COLLECT_ALL,\nshared failures list)
38+
ObjValidator --> Caller: new ObjectValidator(target, chainView)
39+
end
40+
41+
loop each validation step
42+
alt typed reusable step
43+
Caller -> ObjValidator: step(ValidationStep)
44+
ObjValidator -> Step: validate(target, collector)
45+
else collector-only step
46+
Caller -> ObjValidator: step(collector -> ...)
47+
ObjValidator -> Step: accept(collector)
48+
else direct check/require
49+
Caller -> ObjValidator: check(...) / require(...)
50+
ObjValidator -> Collector: check(...) / require(...)
51+
end
52+
53+
alt step uses collector
54+
Step -> Collector: check(...) / require(...)
55+
end
56+
57+
Collector -> Chain: check(valid, failure)\nor check(predicate, failure)
58+
Chain -> Chain: shouldSkipEvaluation()?
59+
60+
alt FAIL_FAST and a previous failure exists
61+
Chain --> Collector: skip evaluation
62+
else current validation passed
63+
Chain --> Collector: no failure added
64+
else current validation failed
65+
Collector -> Failure: ValidationFailure.of(errorCode, message)\n.field(...)\n.rejectedValue(...)\n.violationCode(...)\n.metadata(...)\n.exceptionFactory(...)
66+
Collector -> Chain: add failure
67+
end
68+
end
69+
70+
alt caller asks for result
71+
Caller -> ObjValidator: toResult()
72+
ObjValidator -> Collector: toResult()
73+
Collector -> Chain: toResult()
74+
Chain -> Chain: snapshot failures
75+
Chain -> Failure: toFieldViolation() for each failure
76+
Chain -> Result: new ValidationResult(mode,\nfailures, violations)
77+
Result --> Caller: immutable result
78+
79+
note over Result
80+
Result exposes:
81+
- isValid()
82+
- failures()
83+
- violations()
84+
- metadata() merged from all failures
85+
- firstFailure()
86+
end note
87+
else caller wants exception
88+
Caller -> ObjValidator: throwIfInvalid()\nor throwIfInvalid(customFactory)
89+
ObjValidator -> Collector: throwIfInvalid(...)
90+
Collector -> Chain: throwIfInvalid(...)
91+
Chain -> Chain: result = toResult()
92+
93+
alt result is valid
94+
Chain --> Caller: return normally
95+
else first failure has exceptionFactory
96+
Chain -> Failure: firstFailure.exceptionFactory().apply(firstFailure)
97+
Failure --> Chain: RuntimeException
98+
Chain --> Caller: throw custom exception
99+
else custom terminal factory provided
100+
Chain -> Result: customFactory.apply(result)
101+
Result --> Chain: RuntimeException
102+
Chain --> Caller: throw custom exception
103+
else default behavior
104+
Chain -> Result: firstFailure(), metadata(), violations()
105+
Chain -> BusinessEx: new BusinessException(\nfirstFailure.errorCode(),\nfirstFailure.message(),\nnull,\nresult.metadata(),\nresult.violations())
106+
BusinessEx --> Caller: throw BusinessException
107+
end
108+
end
109+
110+
note over Caller
111+
FAIL_FAST stops recording after first failure.
112+
COLLECT_ALL keeps all failures in one result/exception.
113+
end note
114+
115+
@enduml

error-utils-core/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
<artifactId>junit-jupiter</artifactId>
2121
<scope>test</scope>
2222
</dependency>
23+
<dependency>
24+
<groupId>com.tngtech.archunit</groupId>
25+
<artifactId>archunit-junit5</artifactId>
26+
<scope>test</scope>
27+
</dependency>
2328
<dependency>
2429
<groupId>com.fasterxml.jackson.core</groupId>
2530
<artifactId>jackson-annotations</artifactId>

error-utils-core/src/main/java/com/maslonka/reservation/errorutils/core/api/ApiErrorAssembler.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@
88
*/
99
public class ApiErrorAssembler {
1010

11-
/**
12-
* Creates a stateless assembler instance.
13-
*/
14-
public ApiErrorAssembler() {
15-
}
16-
1711
/**
1812
* Builds an immutable {@link ApiError} from the provided normalized input.
1913
*

0 commit comments

Comments
 (0)