From 7d61c733539b54226b8051d7a680faaf9b885553 Mon Sep 17 00:00:00 2001 From: xvw Date: Tue, 30 Jul 2024 05:32:29 +0200 Subject: [PATCH] A `ring.exe` cli based on `Cmdliner` --- README.md | 19 ++++++- bin/dune | 2 +- bin/ring.ml | 138 +++++++++++++++++++++++++++++++++++++++++++++++--- dune-project | 2 + lib/action.ml | 5 ++ ring.opam | 1 + 6 files changed, 159 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 15f3dab..db2cace 100644 --- a/README.md +++ b/README.md @@ -70,12 +70,29 @@ being development dependencies of the project). ## Run the binary `ring.exe` -Just run the `ring.exe` binary compiled from `bin/ring.ml` like this: +Just run the `ring.exe` binary compiled from `bin/ring.ml`, which should display +the `manpage`, giving information on how to interact with the binary, like this: ```shell dune exec bin/ring.exe ``` +Broadly speaking, here are the two main actions proposed by the `ring.exe` +binary: + +- `dune exec bin/ring.exe` display the manpage of the binary +- `dune exec bin/ring.exe -- build [COMMON_OPTIONS]` builds the ring in `_www` + using the current directory as source +- `dune exec bin/ring.exe -- build [COMMON_OPTIONS] [--port PORT]` launches a + development server that rebuilds the ring each time a page is refreshed + +The common options are: + +- `--target PATH` describes the compilation target (the directory where the ring + is to be built) +- `--source PATH` describes the compilation source (the directory where the data of the ring are located) +- `--log-level (info | app | debug | error | warning)` describes the log-level + ## Launching tests The build procedure is based on `dune`, without any particular sorcery, so diff --git a/bin/dune b/bin/dune index fab60d2..9091135 100644 --- a/bin/dune +++ b/bin/dune @@ -1,4 +1,4 @@ (executable (name ring) (public_name ring) - (libraries yocaml yocaml_eio gem)) + (libraries cmdliner yocaml yocaml_eio gem)) diff --git a/bin/ring.ml b/bin/ring.ml index 7aeabe7..872951c 100644 --- a/bin/ring.ml +++ b/bin/ring.ml @@ -1,8 +1,134 @@ -open Yocaml +let run_build target source log_level = + let module Resolver = Gem.Resolver.Make (struct + let source = source + let target = target + end) in + Yocaml_eio.run ~level:log_level (Gem.Action.process_all (module Resolver)) -module Resolver = Gem.Resolver.Make (struct - let source = Path.rel [] - let target = Path.rel [ "_www" ] -end) +let run_watch target source log_level port = + let module Resolver = Gem.Resolver.Make (struct + let source = source + let target = target + end) in + Yocaml_eio.serve ~target:Resolver.target ~level:log_level ~port + (Gem.Action.process_all (module Resolver)) -let () = Yocaml_eio.run ~level:`Debug (Gem.Action.process_all (module Resolver)) +open Cmdliner + +let exits = Cmd.Exit.defaults +let version = "dev" + +let path_conv = + Arg.conv ~docv:"PATH" + ((fun str -> str |> Yocaml.Path.from_string |> Result.ok), Yocaml.Path.pp) + +let port_conv = + Arg.conv' ~docv:"PORT" + ( (fun str -> + match int_of_string_opt str with + | None -> Result.error (str ^ " is not a valid port value") + | Some x when x < 0 -> Result.error (str ^ " is < 0") + | Some x when x > 9999 -> Result.error (str ^ " is > 9999") + | Some x -> Result.ok x), + fun ppf -> Format.fprintf ppf "%04d" ) + +let log_level_conv = + Arg.conv ~docv:"LEVEL" + ( (fun str -> + match String.(str |> trim |> lowercase_ascii) with + | "app" -> Result.ok `App + | "info" -> Result.ok `Info + | "error" -> Result.ok `Error + | "warning" -> Result.ok `Warning + | _ -> Result.ok `Debug), + fun ppf -> function + | `App -> Format.fprintf ppf "app" + | `Debug -> Format.fprintf ppf "debug" + | `Info -> Format.fprintf ppf "info" + | `Error -> Format.fprintf ppf "error" + | `Warning -> Format.fprintf ppf "warning" ) + +let target_arg = + let default = Yocaml.Path.rel [ "_www" ] in + let doc = "The directory where the ring will be built" in + let arg = + Arg.info ~doc ~docs:Manpage.s_common_options [ "target"; "output" ] + in + Arg.(value (opt path_conv default arg)) + +let source_arg = + let default = Yocaml.Path.rel [] in + let doc = "The directory used as source" in + let arg = + Arg.info ~doc ~docs:Manpage.s_common_options [ "source"; "input" ] + in + Arg.(value (opt path_conv default arg)) + +let port_arg = + let default = 8888 in + let doc = "The port used to serve the development server" in + let arg = Arg.info ~doc ~docs:Manpage.s_common_options [ "port"; "listen" ] in + Arg.(value (opt port_conv default arg)) + +let log_level_arg default = + let doc = + "The log-level (app | info | debug | error | warning), by default" + in + let arg = Arg.info ~doc ~docs:Manpage.s_common_options [ "log-level" ] in + Arg.(value (opt log_level_conv default arg)) + +let bug_report = + "The application's source code is published on \ + . Feel free to contribute or report bugs \ + on ." + +let description = + "ring.muhokama is free software that lets you build a static site that \ + describes a webring, in homage to the webrings of the 1990s, a return to \ + the small-web." + +let build = + let doc = + "Build the ring in a given TARGET, based on a given SOURCE with a given \ + LOG_LEVEL" + in + let man = + [ + `S Manpage.s_description; `P description; `S Manpage.s_bugs; `P bug_report; + ] + in + let info = Cmd.info "build" ~version ~doc ~exits ~man in + let term = + Term.(const run_build $ target_arg $ source_arg $ log_level_arg `Debug) + in + Cmd.v info term + +let watch = + let doc = + "Build the ring and launch the dev-server in a given TARGET, based on a \ + given SOURCE with a given LOG_LEVEL listen to a dedicated PORT" + in + let man = + [ + `S Manpage.s_description; `P description; `S Manpage.s_bugs; `P bug_report; + ] + in + let info = Cmd.info "watch" ~version ~doc ~exits ~man in + let term = + Term.( + const run_watch $ target_arg $ source_arg $ log_level_arg `Info $ port_arg) + in + Cmd.v info term + +let index = + let doc = "ring.muhokama" in + let man = + [ + `S Manpage.s_description; `P description; `S Manpage.s_bugs; `P bug_report; + ] + in + let info = Cmd.info "dune exec bin/ring.exe" ~version ~doc ~man in + let default = Term.(ret (const (`Help (`Pager, None)))) in + Cmd.group info ~default [ build; watch ] + +let () = exit @@ Cmd.eval index diff --git a/dune-project b/dune-project index 2508774..fd96d81 100644 --- a/dune-project +++ b/dune-project @@ -25,6 +25,8 @@ yocaml_eio yocaml_syndication + (cmdliner (= 1.3.0)) + (ppx_expect :with-test) (ocamlformat :with-dev-setup) diff --git a/lib/action.ml b/lib/action.ml index 74be0fa..6ffb06b 100644 --- a/lib/action.ml +++ b/lib/action.ml @@ -21,6 +21,10 @@ let init_chain (module R : Sigs.RESOLVER) = in (cache, Chain.init ~chain ~members) +let init_message (module R : Sigs.RESOLVER) = + Yocaml.Eff.logf ~level:`Debug "ring.muhokama [source: `%a`, target: `%a`]" + Yocaml.Path.pp R.source Yocaml.Path.pp R.target + let final_message _cache = Yocaml.Eff.log ~level:`Debug "ring.muhokama done" let generate_opml (module R : Sigs.RESOLVER) chain = @@ -32,6 +36,7 @@ let generate_opml (module R : Sigs.RESOLVER) chain = let process_all (module R : Sigs.RESOLVER) () = let open Yocaml.Eff in + let* () = init_message (module R) in let* cache, chain = init_chain (module R) in return cache >>= generate_opml (module R) chain diff --git a/ring.opam b/ring.opam index d525505..447289c 100644 --- a/ring.opam +++ b/ring.opam @@ -17,6 +17,7 @@ depends: [ "yocaml_omd" "yocaml_eio" "yocaml_syndication" + "cmdliner" {= "1.3.0"} "ppx_expect" {with-test} "ocamlformat" {with-dev-setup} "ocp-indent" {with-dev-setup}