[OCaml] High Intensity Training Online
test Guard the presenter boundary
Add a guard asserting the presenter (pages.ml, pages.mli) references no inner-circle module — Evidence, Prescription, Exercise, Recovery, Progression, Repository, Trainee, Catalog, or Service. Only view models cross into it. The check matches module access (Name.) so UI label strings like "Exercise" do not trip it. A future change that passes an entity into a page now fails the build.
test/test_architecture.ml
@@ -210,4 +210,61 @@
210
210
(List.mem "hito.persistence" libs) );
211
211
]
212
212
213
Removed:
let suite = [ ("architecture.dependency_rule", dependency_rule) ]
213
Added:
(* The presenter (Pages) is an interface adapter: only view models cross into
214
Added:
it. It must name no core entity and no repository type — those reach it as
215
Added:
plain DTOs from the controller (Handlers). This guard scans the two presenter
216
Added:
files directly, since Handlers in the same layer legitimately names
217
Added:
entities to build the DTOs. *)
218
Added:
let presenter_boundary =
219
Added:
let forbidden =
220
Added:
[
221
Added:
"Evidence";
222
Added:
"Prescription";
223
Added:
"Exercise";
224
Added:
"Recovery";
225
Added:
"Progression";
226
Added:
"Repository";
227
Added:
"Trainee";
228
Added:
"Catalog";
229
Added:
"Service";
230
Added:
]
231
Added:
in
232
Added:
let is_word_char c =
233
Added:
(c >= 'A' && c <= 'Z')
234
Added:
|| (c >= 'a' && c <= 'z')
235
Added:
|| (c >= '0' && c <= '9')
236
Added:
|| c = '_'
237
Added:
in
238
Added:
let mentions name text =
239
Added:
(* Match a module reference: the name followed by '.', with no identifier
240
Added:
char before it. This flags [Evidence.Workout] but not the label string
241
Added:
"Exercise". *)
242
Added:
let n = String.length name and h = String.length text in
243
Added:
let rec scan i =
244
Added:
if i + n >= h then false
245
Added:
else if String.sub text i n = name && text.[i + n] = '.' then
246
Added:
let before = i = 0 || not (is_word_char text.[i - 1]) in
247
Added:
if before then true else scan (i + 1)
248
Added:
else scan (i + 1)
249
Added:
in
250
Added:
scan 0
251
Added:
in
252
Added:
let check file =
253
Added:
( Printf.sprintf "the presenter (%s) names no inner-circle entity"
254
Added:
(Filename.basename file),
255
Added:
`Quick,
256
Added:
fun () ->
257
Added:
let text = read_file (Filename.concat (lib "web") file) in
258
Added:
List.iter
259
Added:
(fun name ->
260
Added:
if mentions name text then
261
Added:
Alcotest.failf "%s names inner-circle %S" file name)
262
Added:
forbidden )
263
Added:
in
264
Added:
[ check "pages.ml"; check "pages.mli" ]
265
Added:
266
Added:
let suite =
267
Added:
[
268
Added:
("architecture.dependency_rule", dependency_rule);
269
Added:
("architecture.presenter_boundary", presenter_boundary);
270
Added:
]