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.

Commit
8af6db974008ef804e96ab5ffccfa47975386b1e
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
bin/main.ml
index 301ddd1e..9de8b90e 100644..100644
@@ -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
index 00000000..bbf971b4 000000..100644
@@ -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
index 0718e6cf..4e958be9 100644..100644
@@ -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
index 96fff447..aa9eeb63 100644..100644
@@ -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
index 22274afc..e26012ad 100644..100644
@@ -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
index 094cd8ed..dc9bba24 100644..100644
@@ -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
index 552bcc59..7f5f7e57 100644..100644
@@ -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