[OCaml] High Intensity Training Online
refactor put the workout importer behind a port
The web controller named the Hevy CSV adapter directly, and hito.web depended on hito.import. That broke the layering rule: a controller never reaches an adapter, and only the composition root names one. An Importer port now lives in hito.app. Handlers.Make takes an implementation as a functor argument, the import controller calls only I.parse and I.pp_error, and hito.web no longer depends on hito.import. The composition root and the test support plug Hevy_csv in. The build graph now forbids what convention only asked for.
Changed files
bin/main.ml
@@ -31,6 +31,7 @@
31
31
Hito_web.Handlers.Make
32
32
(Hito_persistence.Sqlite_repo)
33
33
(Hito_persistence.Bcrypt_hash)
34
Added:
(Hito_import.Hevy_csv)
34
35
35
36
let getenv name default =
36
37
match Sys.getenv_opt name with Some v when v <> "" -> v | _ -> default
lib/app/importer.ml
@@ -0,0 +1,24 @@
1
Added:
(** The workout-import port. An adapter parses one third-party export format
2
Added:
into a provisional {!Workout_import.batch}. The web layer names only this
3
Added:
port, so no adapter (and no parser library) is reachable from a controller.
4
Added:
The composition root plugs a concrete adapter in.
5
Added:
6
Added:
Errors are the adapter's own type. The web layer only phrases them through
7
Added:
[pp_error], so a new export format needs no change to the port. *)
8
Added:
9
Added:
module type S = sig
10
Added:
type error
11
Added:
12
Added:
val pp_error : Format.formatter -> error -> unit
13
Added:
14
Added:
val parse :
15
Added:
batch_id:Workout_import.batch_id ->
16
Added:
fingerprint:string ->
17
Added:
imported_at:Recovery.timestamp ->
18
Added:
utc_offset_seconds:int ->
19
Added:
string ->
20
Added:
(Workout_import.batch, error) result
21
Added:
(** Parse one export. [utc_offset_seconds] is the offset of the export's
22
Added:
wall-clock times from UTC. The batch is provisional: it is stored for
23
Added:
review, and promotion happens later through the service. *)
24
Added:
end
lib/web/dune
@@ -45,12 +45,4 @@
45
45
workout_client_asset)
46
46
(preprocess
47
47
(pps dream-html.ppx))
48
Removed:
(libraries
49
Removed:
hito.core
50
Removed:
hito.app
51
Removed:
hito.import
52
Removed:
hito.presenter
53
Removed:
dream
54
Removed:
dream-html
55
Removed:
lwt
56
Removed:
unix))
48
Added:
(libraries hito.core hito.app hito.presenter dream dream-html lwt unix))
lib/web/handlers.ml
@@ -8,7 +8,7 @@
8
8
and [Present] types are identical. It then assembles the route table and the
9
9
error handler. See [handlers.mli] for the public surface. *)
10
10
11
Removed:
module Make (R : Repository.S) (H : Password_hash.S) = struct
11
Added:
module Make (R : Repository.S) (H : Password_hash.S) (I : Importer.S) = struct
12
12
module Kit = Handlers_kit.Make (R) (H)
13
13
include Kit
14
14
module Auth = Handlers_auth.Make (R) (H) (Kit)
@@ -18,7 +18,7 @@
18
18
module App_feedback = Handlers_app_feedback.Make (R) (H) (Kit)
19
19
module Profile = Handlers_profile.Make (R) (H) (Kit)
20
20
module Settings = Handlers_settings.Make (R) (H) (Kit)
21
Removed:
module Import = Handlers_import.Make (R) (H) (Kit)
21
Added:
module Import = Handlers_import.Make (R) (H) (I) (Kit)
22
22
module Assets = Handlers_assets
23
23
24
24
(* A catch-all for any path no route claims. It renders the branded 404 so an
lib/web/handlers.mli
@@ -2,7 +2,10 @@
2
2
the repository so tests drive it with an in-memory store while production
3
3
uses SQLite. *)
4
4
5
Removed:
module Make (R : Hito_app.Repository.S) (_ : Hito_app.Password_hash.S) : sig
5
Added:
module Make
6
Added:
(R : Hito_app.Repository.S)
7
Added:
(_ : Hito_app.Password_hash.S)
8
Added:
(_ : Hito_app.Importer.S) : sig
6
9
type t
7
10
8
11
val make :
lib/web/handlers_import.ml
@@ -5,6 +5,7 @@
5
5
module Make
6
6
(R : Repository.S)
7
7
(H : Password_hash.S)
8
Added:
(I : Importer.S)
8
9
(Kit : module type of Handlers_kit.Make (R) (H)) =
9
10
struct
10
11
open Kit
@@ -126,13 +127,12 @@
126
127
Workout_import.batch_id (generate_batch_id ~fingerprint ~imported_at)
127
128
in
128
129
match
129
Removed:
Hito_import.Hevy_csv.parse ~batch_id ~fingerprint ~imported_at
130
Removed:
~utc_offset_seconds source
130
Added:
I.parse ~batch_id ~fingerprint ~imported_at ~utc_offset_seconds source
131
131
with
132
132
| Error e ->
133
133
redirect_with_flash request
134
134
(Dream_html.path_attr Dream_html.HTML.href Routes.profile)
135
Removed:
(Format.asprintf "%a" Hito_import.Hevy_csv.pp_error e)
135
Added:
(Format.asprintf "%a" I.pp_error e)
136
136
| Ok batch -> (
137
137
let allow_duplicate = if allow_duplicate then Some () else None in
138
138
Service.store_import_batch t.service trainee.Trainee.id
test/web/support.ml
@@ -6,7 +6,9 @@
6
6
7
7
open Hito_app
8
8
open Hito_persistence
9
Removed:
module Handlers = Hito_web.Handlers.Make (Memory_repo) (Bcrypt_hash)
9
Added:
10
Added:
module Handlers =
11
Added:
Hito_web.Handlers.Make (Memory_repo) (Bcrypt_hash) (Hito_import.Hevy_csv)
10
12
11
13
(* A fresh, fully wired application: secret, in-memory sessions, routes.
12
14
Registration is opened here so the auth-flow tests can create trainees via