diff options
Diffstat (limited to 'aoc2023/build/packages/glint')
19 files changed, 2617 insertions, 0 deletions
diff --git a/aoc2023/build/packages/glint/LICENSE b/aoc2023/build/packages/glint/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/aoc2023/build/packages/glint/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/aoc2023/build/packages/glint/README.md b/aoc2023/build/packages/glint/README.md new file mode 100644 index 0000000..d6d5821 --- /dev/null +++ b/aoc2023/build/packages/glint/README.md @@ -0,0 +1,104 @@ +# glint + +[](https://hex.pm/packages/glint) +[](https://hex.pm/packages/glint) +[](https://hexdocs.pm/glint/) +[](https://github.com/tanklesxl/glint/actions) + +Gleam command line argument parsing with basic flag support. + +## Installation + +To install from hex: + +```sh +gleam add glint +``` + +## Usage + +### Glint's Core + +`glint` is conceptually quite small, your general flow will be: + +1. create a new glint instance with `glint.new` +1. configure it with `glint.with_pretty_help` and other configuration functions +1. add commands with `glint.add` + 1. create a new command with `glint.cmd` + 1. assign that command any flags required + 1. assign the command a custom description +1. run your cli with `glnt.run`, run with a function to handle command output with `glint.run_and_handle` + +### Mini Example + +You can import `glint` as a dependency and use it to build simple command-line applications like the following simplified version of the [the hello world example](https://github.com/TanklesXL/glint/tree/main/examples/hello/README.md) + +```gleam +// stdlib imports +import gleam/io +import gleam/list +import gleam/result +import gleam/string.{uppercase} +// external dep imports +import snag +// glint imports +import glint +import glint/flag +// erlang-specific imports + +@target(erlang) +import gleam/erlang.{start_arguments} + +/// the key for the caps flag +const caps = "caps" + +/// a boolean flag with default False to control message capitalization. +/// +fn caps_flag() -> flag.FlagBuilder(Bool) { + flag.bool() + |> flag.default(False) + |> flag.description("Capitalize the provided name") +} + +/// the command function that will be executed +/// +fn hello(input: glint.CommandInput) -> Nil { + let assert Ok(caps) = flag.get_bool(from: input.flags, for: caps) + + let name = + case input.args { + [] -> "Joe" + [name,..] -> name + } + + let msg = "Hello, " <> name <> "!" + + + case caps { + True -> uppercase(msg) + False -> msg + } + |> io.println +} + +pub fn main() { + // create a new glint instance + glint.new() + // with an app name of "hello", this is used when printing help text + |> glint.with_name("hello") + // with pretty help enabled, using the built-in colours + |> glint.with_pretty_help(glint.default_pretty_help()) + // with a root command that executes the `hello` function + |> glint.add( + // add the command to the root + at: [], + // create the command, add any flags + do: glint.command(hello) + // with flag `caps` + |> glint.flag(caps, caps_flag()) + // with a short description + |> glint.description("Prints Hello, <NAME>!"), + ) + |> glint.run(start_arguments()) +} +``` diff --git a/aoc2023/build/packages/glint/gleam.toml b/aoc2023/build/packages/glint/gleam.toml new file mode 100644 index 0000000..e8ac4ae --- /dev/null +++ b/aoc2023/build/packages/glint/gleam.toml @@ -0,0 +1,23 @@ +name = "glint" +version = "0.13.0" + +# Fill out these fields if you intend to generate HTML documentation or publishname = "glint" +# your project to the Hex package manager. +# +licences = ["Apache-2.0"] +description = "Gleam command line argument parsing with basic flag support." +repository = { type = "github", user = "TanklesXL", repo = "glint" } +links = [ + { title = "Hex", href = "https://hex.pm/packages/glint" }, + { title = "Docs", href = "https://hexdocs.pm/glint/" }, +] +gleam = ">= 0.32.0" + +[dependencies] +gleam_stdlib = "~> 0.19" +snag = "~> 0.2" +gleam_community_ansi = "~> 1.0" +gleam_community_colour = "~> 1.0" + +[dev-dependencies] +gleeunit = "~> 0.5" diff --git a/aoc2023/build/packages/glint/include/glint@flag_Flag.hrl b/aoc2023/build/packages/glint/include/glint@flag_Flag.hrl new file mode 100644 index 0000000..645cb12 --- /dev/null +++ b/aoc2023/build/packages/glint/include/glint@flag_Flag.hrl @@ -0,0 +1 @@ +-record(flag, {value :: glint@flag:value(), description :: binary()}). diff --git a/aoc2023/build/packages/glint/include/glint@flag_FlagBuilder.hrl b/aoc2023/build/packages/glint/include/glint@flag_FlagBuilder.hrl new file mode 100644 index 0000000..b5e21a2 --- /dev/null +++ b/aoc2023/build/packages/glint/include/glint@flag_FlagBuilder.hrl @@ -0,0 +1,6 @@ +-record(flag_builder, { + desc :: binary(), + parser :: fun((binary()) -> {ok, any()} | {error, snag:snag()}), + value :: fun((glint@flag:internal(any())) -> glint@flag:value()), + default :: gleam@option:option(any()) +}). diff --git a/aoc2023/build/packages/glint/include/glint@flag_Internal.hrl b/aoc2023/build/packages/glint/include/glint@flag_Internal.hrl new file mode 100644 index 0000000..281bbd0 --- /dev/null +++ b/aoc2023/build/packages/glint/include/glint@flag_Internal.hrl @@ -0,0 +1,4 @@ +-record(internal, { + value :: gleam@option:option(any()), + parser :: fun((binary()) -> {ok, any()} | {error, snag:snag()}) +}). diff --git a/aoc2023/build/packages/glint/include/glint_Command.hrl b/aoc2023/build/packages/glint/include/glint_Command.hrl new file mode 100644 index 0000000..00a03e3 --- /dev/null +++ b/aoc2023/build/packages/glint/include/glint_Command.hrl @@ -0,0 +1,5 @@ +-record(command, { + do :: fun((glint:command_input()) -> any()), + flags :: gleam@map:map_(binary(), glint@flag:flag()), + description :: binary() +}). diff --git a/aoc2023/build/packages/glint/include/glint_CommandInput.hrl b/aoc2023/build/packages/glint/include/glint_CommandInput.hrl new file mode 100644 index 0000000..72c9641 --- /dev/null +++ b/aoc2023/build/packages/glint/include/glint_CommandInput.hrl @@ -0,0 +1,4 @@ +-record(command_input, { + args :: list(binary()), + flags :: gleam@map:map_(binary(), glint@flag:flag()) +}). diff --git a/aoc2023/build/packages/glint/include/glint_Config.hrl b/aoc2023/build/packages/glint/include/glint_Config.hrl new file mode 100644 index 0000000..70cf645 --- /dev/null +++ b/aoc2023/build/packages/glint/include/glint_Config.hrl @@ -0,0 +1,4 @@ +-record(config, { + pretty_help :: gleam@option:option(glint:pretty_help()), + name :: gleam@option:option(binary()) +}). diff --git a/aoc2023/build/packages/glint/include/glint_Glint.hrl b/aoc2023/build/packages/glint/include/glint_Glint.hrl new file mode 100644 index 0000000..7ece11d --- /dev/null +++ b/aoc2023/build/packages/glint/include/glint_Glint.hrl @@ -0,0 +1,5 @@ +-record(glint, { + config :: glint:config(), + cmd :: glint:command_node(any()), + global_flags :: gleam@map:map_(binary(), glint@flag:flag()) +}). diff --git a/aoc2023/build/packages/glint/include/glint_PrettyHelp.hrl b/aoc2023/build/packages/glint/include/glint_PrettyHelp.hrl new file mode 100644 index 0000000..79bd887 --- /dev/null +++ b/aoc2023/build/packages/glint/include/glint_PrettyHelp.hrl @@ -0,0 +1,5 @@ +-record(pretty_help, { + usage :: gleam_community@colour:colour(), + flags :: gleam_community@colour:colour(), + subcommands :: gleam_community@colour:colour() +}). diff --git a/aoc2023/build/packages/glint/include/glint_Stub.hrl b/aoc2023/build/packages/glint/include/glint_Stub.hrl new file mode 100644 index 0000000..5aa5d83 --- /dev/null +++ b/aoc2023/build/packages/glint/include/glint_Stub.hrl @@ -0,0 +1,6 @@ +-record(stub, { + path :: list(binary()), + run :: fun((glint:command_input()) -> any()), + flags :: list({binary(), glint@flag:flag()}), + description :: binary() +}). diff --git a/aoc2023/build/packages/glint/src/glint.app.src b/aoc2023/build/packages/glint/src/glint.app.src new file mode 100644 index 0000000..7eb7649 --- /dev/null +++ b/aoc2023/build/packages/glint/src/glint.app.src @@ -0,0 +1,13 @@ +{application, glint, [ + {vsn, "0.13.0"}, + {applications, [gleam_community_ansi, + gleam_community_colour, + gleam_stdlib, + gleeunit, + snag]}, + {description, "Gleam command line argument parsing with basic flag support."}, + {modules, [glint, + glint@flag, + glint@flag@constraint]}, + {registered, []} +]}. diff --git a/aoc2023/build/packages/glint/src/glint.erl b/aoc2023/build/packages/glint/src/glint.erl new file mode 100644 index 0000000..0501cc6 --- /dev/null +++ b/aoc2023/build/packages/glint/src/glint.erl @@ -0,0 +1,513 @@ +-module(glint). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([with_config/2, with_pretty_help/2, without_pretty_help/1, with_name/2, new/0, command/1, description/2, flag/3, flag_tuple/2, flags/2, global_flag/3, global_flag_tuple/2, global_flags/2, default_pretty_help/0, add/3, help_flag/0, execute/2, run_and_handle/3, run/2, add_command_from_stub/2]). +-export_type([config/0, pretty_help/0, glint/1, command/1, command_input/0, command_node/1, out/1, stub/1]). + +-type config() :: {config, + gleam@option:option(pretty_help()), + gleam@option:option(binary())}. + +-type pretty_help() :: {pretty_help, + gleam_community@colour:colour(), + gleam_community@colour:colour(), + gleam_community@colour:colour()}. + +-opaque glint(GHR) :: {glint, + config(), + command_node(GHR), + gleam@map:map_(binary(), glint@flag:flag())}. + +-opaque command(GHS) :: {command, + fun((command_input()) -> GHS), + gleam@map:map_(binary(), glint@flag:flag()), + binary()}. + +-type command_input() :: {command_input, + list(binary()), + gleam@map:map_(binary(), glint@flag:flag())}. + +-type command_node(GHT) :: {command_node, + gleam@option:option(command(GHT)), + gleam@map:map_(binary(), command_node(GHT))}. + +-type out(GHU) :: {out, GHU} | {help, binary()}. + +-type stub(GHV) :: {stub, + list(binary()), + fun((command_input()) -> GHV), + list({binary(), glint@flag:flag()}), + binary()}. + +-spec with_config(glint(GIA), config()) -> glint(GIA). +with_config(Glint, Config) -> + erlang:setelement(2, Glint, Config). + +-spec with_pretty_help(glint(GID), pretty_help()) -> glint(GID). +with_pretty_help(Glint, Pretty) -> + _pipe = erlang:setelement(2, erlang:element(2, Glint), {some, Pretty}), + with_config(Glint, _pipe). + +-spec without_pretty_help(glint(GIG)) -> glint(GIG). +without_pretty_help(Glint) -> + _pipe = erlang:setelement(2, erlang:element(2, Glint), none), + with_config(Glint, _pipe). + +-spec with_name(glint(GIJ), binary()) -> glint(GIJ). +with_name(Glint, Name) -> + _pipe = erlang:setelement(3, erlang:element(2, Glint), {some, Name}), + with_config(Glint, _pipe). + +-spec empty_command() -> command_node(any()). +empty_command() -> + {command_node, none, gleam@map:new()}. + +-spec new() -> glint(any()). +new() -> + {glint, {config, none, none}, empty_command(), gleam@map:new()}. + +-spec do_add(command_node(GIT), list(binary()), command(GIT)) -> command_node(GIT). +do_add(Root, Path, Contents) -> + case Path of + [] -> + erlang:setelement(2, Root, {some, Contents}); + + [X | Xs] -> + erlang:setelement( + 3, + Root, + (gleam@map:update( + erlang:element(3, Root), + X, + fun(Node) -> _pipe = Node, + _pipe@1 = gleam@option:lazy_unwrap( + _pipe, + fun empty_command/0 + ), + do_add(_pipe@1, Xs, Contents) end + )) + ) + end. + +-spec command(fun((command_input()) -> GJC)) -> command(GJC). +command(Runner) -> + {command, Runner, gleam@map:new(), <<""/utf8>>}. + +-spec description(command(GJF), binary()) -> command(GJF). +description(Cmd, Description) -> + erlang:setelement(4, Cmd, Description). + +-spec flag(command(GJI), binary(), glint@flag:flag_builder(any())) -> command(GJI). +flag(Cmd, Key, Flag) -> + erlang:setelement( + 3, + Cmd, + gleam@map:insert(erlang:element(3, Cmd), Key, glint@flag:build(Flag)) + ). + +-spec flag_tuple(command(GJN), {binary(), glint@flag:flag_builder(any())}) -> command(GJN). +flag_tuple(Cmd, Tup) -> + flag(Cmd, erlang:element(1, Tup), erlang:element(2, Tup)). + +-spec flags(command(GJS), list({binary(), glint@flag:flag()})) -> command(GJS). +flags(Cmd, Flags) -> + gleam@list:fold( + Flags, + Cmd, + fun(Cmd@1, _use1) -> + {Key, Flag} = _use1, + erlang:setelement( + 3, + Cmd@1, + gleam@map:insert(erlang:element(3, Cmd@1), Key, Flag) + ) + end + ). + +-spec global_flag(glint(GJW), binary(), glint@flag:flag_builder(any())) -> glint(GJW). +global_flag(Glint, Key, Flag) -> + erlang:setelement( + 4, + Glint, + gleam@map:insert(erlang:element(4, Glint), Key, glint@flag:build(Flag)) + ). + +-spec global_flag_tuple(glint(GKB), {binary(), glint@flag:flag_builder(any())}) -> glint(GKB). +global_flag_tuple(Glint, Tup) -> + global_flag(Glint, erlang:element(1, Tup), erlang:element(2, Tup)). + +-spec global_flags(glint(GKG), list({binary(), glint@flag:flag()})) -> glint(GKG). +global_flags(Glint, Flags) -> + erlang:setelement( + 4, + Glint, + (gleam@list:fold( + Flags, + erlang:element(4, Glint), + fun(Acc, Tup) -> + gleam@map:insert( + Acc, + erlang:element(1, Tup), + erlang:element(2, Tup) + ) + end + )) + ). + +-spec execute_root( + command_node(GKU), + gleam@map:map_(binary(), glint@flag:flag()), + list(binary()), + list(binary()) +) -> {ok, out(GKU)} | {error, snag:snag()}. +execute_root(Cmd, Global_flags, Args, Flag_inputs) -> + _pipe@3 = case erlang:element(2, Cmd) of + {some, Contents} -> + gleam@result:'try'( + gleam@list:try_fold( + Flag_inputs, + gleam@map:merge(Global_flags, erlang:element(3, Contents)), + fun glint@flag:update_flags/2 + ), + fun(New_flags) -> _pipe = {command_input, Args, New_flags}, + _pipe@1 = (erlang:element(2, Contents))(_pipe), + _pipe@2 = {out, _pipe@1}, + {ok, _pipe@2} end + ); + + none -> + snag:error(<<"command not found"/utf8>>) + end, + snag:context(_pipe@3, <<"failed to run command"/utf8>>). + +-spec default_pretty_help() -> pretty_help(). +default_pretty_help() -> + _assert_subject = gleam_community@colour:from_rgb255(182, 255, 234), + {ok, Usage_colour} = case _assert_subject of + {ok, _} -> _assert_subject; + _assert_fail -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail, + module => <<"glint"/utf8>>, + function => <<"default_pretty_help"/utf8>>, + line => 404}) + end, + _assert_subject@1 = gleam_community@colour:from_rgb255(255, 175, 243), + {ok, Flags_colour} = case _assert_subject@1 of + {ok, _} -> _assert_subject@1; + _assert_fail@1 -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail@1, + module => <<"glint"/utf8>>, + function => <<"default_pretty_help"/utf8>>, + line => 405}) + end, + _assert_subject@2 = gleam_community@colour:from_rgb255(252, 226, 174), + {ok, Subcommands_colour} = case _assert_subject@2 of + {ok, _} -> _assert_subject@2; + _assert_fail@2 -> + erlang:error(#{gleam_error => let_assert, + message => <<"Assertion pattern match failed"/utf8>>, + value => _assert_fail@2, + module => <<"glint"/utf8>>, + function => <<"default_pretty_help"/utf8>>, + line => 406}) + end, + {pretty_help, Usage_colour, Flags_colour, Subcommands_colour}. + +-spec is_not_empty(binary()) -> boolean(). +is_not_empty(S) -> + S /= <<""/utf8>>. + +-spec sanitize_path(list(binary())) -> list(binary()). +sanitize_path(Path) -> + _pipe = Path, + _pipe@1 = gleam@list:map(_pipe, fun gleam@string:trim/1), + gleam@list:filter(_pipe@1, fun is_not_empty/1). + +-spec add(glint(GIO), list(binary()), command(GIO)) -> glint(GIO). +add(Glint, Path, Contents) -> + erlang:setelement( + 3, + Glint, + begin + _pipe = Path, + _pipe@1 = sanitize_path(_pipe), + do_add(erlang:element(3, Glint), _pipe@1, Contents) + end + ). + +-spec help_flag() -> binary(). +help_flag() -> + <<(<<"--"/utf8>>)/binary, "help"/utf8>>. + +-spec wrap_with_space(binary()) -> binary(). +wrap_with_space(S) -> + case S of + <<""/utf8>> -> + <<" "/utf8>>; + + _ -> + <<<<" "/utf8, S/binary>>/binary, " "/utf8>> + end. + +-spec subcommand_help(binary(), command_node(any())) -> binary(). +subcommand_help(Name, Cmd) -> + case erlang:element(2, Cmd) of + none -> + Name; + + {some, Contents} -> + <<<<Name/binary, "\t\t"/utf8>>/binary, + (erlang:element(4, Contents))/binary>> + end. + +-spec subcommands_help(gleam@map:map_(binary(), command_node(any()))) -> binary(). +subcommands_help(Cmds) -> + _pipe = Cmds, + _pipe@1 = gleam@map:map_values(_pipe, fun subcommand_help/2), + _pipe@2 = gleam@map:values(_pipe@1), + _pipe@3 = gleam@list:sort(_pipe@2, fun gleam@string:compare/2), + gleam@string:join(_pipe@3, <<"\n\t"/utf8>>). + +-spec heading_style(binary(), gleam_community@colour:colour()) -> binary(). +heading_style(Heading, Colour) -> + _pipe = Heading, + _pipe@1 = gleam_community@ansi:bold(_pipe), + _pipe@2 = gleam_community@ansi:underline(_pipe@1), + _pipe@3 = gleam_community@ansi:italic(_pipe@2), + _pipe@4 = gleam_community@ansi:hex( + _pipe@3, + gleam_community@colour:to_rgb_hex(Colour) + ), + gleam_community@ansi:reset(_pipe@4). + +-spec usage_help( + binary(), + gleam@map:map_(binary(), glint@flag:flag()), + config() +) -> binary(). +usage_help(Cmd_name, Flags, Config) -> + App_name = gleam@option:unwrap( + erlang:element(3, Config), + <<"gleam run"/utf8>> + ), + Flags@1 = begin + _pipe = Flags, + _pipe@1 = gleam@map:to_list(_pipe), + _pipe@2 = gleam@list:map(_pipe@1, fun glint@flag:flag_type_help/1), + gleam@list:sort(_pipe@2, fun gleam@string:compare/2) + end, + Flag_sb = case Flags@1 of + [] -> + gleam@string_builder:new(); + + _ -> + _pipe@3 = Flags@1, + _pipe@4 = gleam@list:intersperse(_pipe@3, <<" "/utf8>>), + _pipe@5 = gleam@string_builder:from_strings(_pipe@4), + _pipe@6 = gleam@string_builder:prepend(_pipe@5, <<" [ "/utf8>>), + gleam@string_builder:append(_pipe@6, <<" ]"/utf8>>) + end, + _pipe@7 = [App_name, wrap_with_space(Cmd_name), <<"[ ARGS ]"/utf8>>], + _pipe@8 = gleam@string_builder:from_strings(_pipe@7), + _pipe@9 = gleam@string_builder:append_builder(_pipe@8, Flag_sb), + _pipe@12 = gleam@string_builder:prepend( + _pipe@9, + <<(begin + _pipe@10 = erlang:element(2, Config), + _pipe@11 = gleam@option:map( + _pipe@10, + fun(Styling) -> + heading_style( + <<"USAGE:"/utf8>>, + erlang:element(2, Styling) + ) + end + ), + gleam@option:unwrap(_pipe@11, <<"USAGE:"/utf8>>) + end)/binary, + "\n\t"/utf8>> + ), + gleam@string_builder:to_string(_pipe@12). + +-spec cmd_help( + list(binary()), + command_node(any()), + config(), + gleam@map:map_(binary(), glint@flag:flag()) +) -> binary(). +cmd_help(Path, Cmd, Config, Global_flags) -> + Name = begin + _pipe = Path, + _pipe@1 = gleam@list:reverse(_pipe), + gleam@string:join(_pipe@1, <<" "/utf8>>) + end, + Flags = begin + _pipe@2 = gleam@option:map( + erlang:element(2, Cmd), + fun(Contents) -> erlang:element(3, Contents) end + ), + _pipe@3 = gleam@option:lazy_unwrap(_pipe@2, fun gleam@map:new/0), + gleam@map:merge(Global_flags, _pipe@3) + end, + Flags_help_body = <<<<(begin + _pipe@4 = erlang:element(2, Config), + _pipe@5 = gleam@option:map( + _pipe@4, + fun(P) -> + heading_style(<<"FLAGS:"/utf8>>, erlang:element(3, P)) + end + ), + gleam@option:unwrap(_pipe@5, <<"FLAGS:"/utf8>>) + end)/binary, + "\n\t"/utf8>>/binary, + (gleam@string:join( + gleam@list:sort( + [<<"--help\t\t\tPrint help information"/utf8>> | + glint@flag:flags_help(Flags)], + fun gleam@string:compare/2 + ), + <<"\n\t"/utf8>> + ))/binary>>, + Usage = usage_help(Name, Flags, Config), + Description = begin + _pipe@6 = erlang:element(2, Cmd), + _pipe@7 = gleam@option:map( + _pipe@6, + fun(Contents@1) -> erlang:element(4, Contents@1) end + ), + gleam@option:unwrap(_pipe@7, <<""/utf8>>) + end, + Header_items = begin + _pipe@8 = [Name, Description], + _pipe@9 = gleam@list:filter(_pipe@8, fun is_not_empty/1), + gleam@string:join(_pipe@9, <<"\n"/utf8>>) + end, + Subcommands = case subcommands_help(erlang:element(3, Cmd)) of + <<""/utf8>> -> + <<""/utf8>>; + + Subcommands_help_body -> + <<<<(begin + _pipe@10 = erlang:element(2, Config), + _pipe@11 = gleam@option:map( + _pipe@10, + fun(P@1) -> + heading_style( + <<"SUBCOMMANDS:"/utf8>>, + erlang:element(4, P@1) + ) + end + ), + gleam@option:unwrap(_pipe@11, <<"SUBCOMMANDS:"/utf8>>) + end)/binary, + "\n\t"/utf8>>/binary, + Subcommands_help_body/binary>> + end, + _pipe@12 = [Header_items, Usage, Flags_help_body, Subcommands], + _pipe@13 = gleam@list:filter(_pipe@12, fun is_not_empty/1), + gleam@string:join(_pipe@13, <<"\n\n"/utf8>>). + +-spec do_execute( + command_node(GKO), + config(), + gleam@map:map_(binary(), glint@flag:flag()), + list(binary()), + list(binary()), + boolean(), + list(binary()) +) -> {ok, out(GKO)} | {error, snag:snag()}. +do_execute(Cmd, Config, Global_flags, Args, Flags, Help, Command_path) -> + case Args of + [] when Help -> + _pipe = Command_path, + _pipe@1 = cmd_help(_pipe, Cmd, Config, Global_flags), + _pipe@2 = {help, _pipe@1}, + {ok, _pipe@2}; + + [] -> + execute_root(Cmd, Global_flags, [], Flags); + + [Arg | Rest] -> + case gleam@map:get(erlang:element(3, Cmd), Arg) of + {ok, Cmd@1} -> + do_execute( + Cmd@1, + Config, + Global_flags, + Rest, + Flags, + Help, + [Arg | Command_path] + ); + + _ when Help -> + _pipe@3 = Command_path, + _pipe@4 = cmd_help(_pipe@3, Cmd, Config, Global_flags), + _pipe@5 = {help, _pipe@4}, + {ok, _pipe@5}; + + _ -> + execute_root(Cmd, Global_flags, Args, Flags) + end + end. + +-spec execute(glint(GKK), list(binary())) -> {ok, out(GKK)} | + {error, snag:snag()}. +execute(Glint, Args) -> + Help_flag = help_flag(), + {Help, Args@2} = case gleam@list:pop(Args, fun(S) -> S =:= Help_flag end) of + {ok, {_, Args@1}} -> + {true, Args@1}; + + _ -> + {false, Args} + end, + {Flags, Args@3} = gleam@list:partition( + Args@2, + fun(_capture) -> gleam@string:starts_with(_capture, <<"--"/utf8>>) end + ), + do_execute( + erlang:element(3, Glint), + erlang:element(2, Glint), + erlang:element(4, Glint), + Args@3, + Flags, + Help, + [] + ). + +-spec run_and_handle(glint(GLC), list(binary()), fun((GLC) -> any())) -> nil. +run_and_handle(Glint, Args, Handle) -> + case execute(Glint, Args) of + {error, Err} -> + _pipe = Err, + _pipe@1 = snag:pretty_print(_pipe), + gleam@io:println(_pipe@1); + + {ok, {help, Help}} -> + gleam@io:println(Help); + + {ok, {out, Out}} -> + Handle(Out), + nil + end. + +-spec run(glint(any()), list(binary())) -> nil. +run(Glint, Args) -> + run_and_handle(Glint, Args, gleam@function:constant(nil)). + +-spec add_command_from_stub(glint(GLP), stub(GLP)) -> glint(GLP). +add_command_from_stub(Glint, Stub) -> + add( + Glint, + erlang:element(2, Stub), + begin + _pipe = command(erlang:element(3, Stub)), + _pipe@1 = flags(_pipe, erlang:element(4, Stub)), + description(_pipe@1, erlang:element(5, Stub)) + end + ). diff --git a/aoc2023/build/packages/glint/src/glint.gleam b/aoc2023/build/packages/glint/src/glint.gleam new file mode 100644 index 0000000..b159016 --- /dev/null +++ b/aoc2023/build/packages/glint/src/glint.gleam @@ -0,0 +1,588 @@ +import gleam/map.{type Map} +import gleam/option.{type Option, None, Some} +import gleam/list +import gleam/io +import gleam/string +import snag.{type Result} +import glint/flag.{type Flag, type Map as FlagMap} +import gleam/string_builder as sb +import gleam_community/ansi +import gleam_community/colour.{type Colour} +import gleam/result +import gleam/function + +// --- CONFIGURATION --- + +// -- CONFIGURATION: TYPES -- + +/// Config for glint +/// +pub type Config { + Config(pretty_help: Option(PrettyHelp), name: Option(String)) +} + +/// PrettyHelp defines the header colours to be used when styling help text +/// +pub type PrettyHelp { + PrettyHelp(usage: Colour, flags: Colour, subcommands: Colour) +} + +// -- CONFIGURATION: CONSTANTS -- + +/// Default config +/// +pub const default_config = Config(pretty_help: None, name: None) + +// -- CONFIGURATION: FUNCTIONS -- + +/// Add the provided config to the existing command tree +/// +pub fn with_config(glint: Glint(a), config: Config) -> Glint(a) { + Glint(..glint, config: config) +} + +/// Enable custom colours for help text headers +/// For a pre-made colouring use `default_pretty_help()` +/// +pub fn with_pretty_help(glint: Glint(a), pretty: PrettyHelp) -> Glint(a) { + Config(..glint.config, pretty_help: Some(pretty)) + |> with_config(glint, _) +} + +/// Disable custom colours for help text headers +/// +pub fn without_pretty_help(glint: Glint(a)) -> Glint(a) { + Config(..glint.config, pretty_help: None) + |> with_config(glint, _) +} + +pub fn with_name(glint: Glint(a), name: String) -> Glint(a) { + Config(..glint.config, name: Some(name)) + |> with_config(glint, _) +} + +// --- CORE --- + +// -- CORE: TYPES -- + +/// Glint container type for config and commands +/// +pub opaque type Glint(a) { + Glint(config: Config, cmd: CommandNode(a), global_flags: FlagMap) +} + +/// CommandNode contents +/// +pub opaque type Command(a) { + Command(do: Runner(a), flags: FlagMap, description: String) +} + +/// Input type for `Runner`. +/// +pub type CommandInput { + CommandInput(args: List(String), flags: FlagMap) +} + +/// Function type to be run by `glint`. +/// +pub type Runner(a) = + fn(CommandInput) -> a + +/// CommandNode tree representation. +/// +type CommandNode(a) { + CommandNode( + contents: Option(Command(a)), + subcommands: Map(String, CommandNode(a)), + ) +} + +/// Ok type for command execution +/// +pub type Out(a) { + /// Container for the command return value + Out(a) + /// Container for the generated help string + Help(String) +} + +/// Result type for command execution +/// +pub type CmdResult(a) = + Result(Out(a)) + +// -- CORE: BUILDER FUNCTIONS -- + +/// Creates a new command tree. +/// +pub fn new() -> Glint(a) { + Glint(config: default_config, cmd: empty_command(), global_flags: map.new()) +} + +/// Adds a new command to be run at the specified path. +/// +/// If the path is `[]`, the root command is set with the provided function and +/// flags. +/// +/// Note: all command paths are sanitized by stripping whitespace and removing any empty string elements. +/// +pub fn add( + to glint: Glint(a), + at path: List(String), + do contents: Command(a), +) -> Glint(a) { + Glint( + ..glint, + cmd: path + |> sanitize_path + |> do_add(to: glint.cmd, put: contents), + ) +} + +/// Recursive traversal of the command tree to find where to puth the provided command +/// +fn do_add( + to root: CommandNode(a), + at path: List(String), + put contents: Command(a), +) -> CommandNode(a) { + case path { + // update current command with provided contents + [] -> CommandNode(..root, contents: Some(contents)) + // continue down the path, creating empty command nodes along the way + [x, ..xs] -> + CommandNode( + ..root, + subcommands: { + use node <- map.update(root.subcommands, x) + node + |> option.lazy_unwrap(empty_command) + |> do_add(xs, contents) + }, + ) + } +} + +/// Helper for initializing empty commands +/// +fn empty_command() -> CommandNode(a) { + CommandNode(contents: None, subcommands: map.new()) +} + +/// Trim each path element and remove any resulting empty strings. +/// +fn sanitize_path(path: List(String)) -> List(String) { + path + |> list.map(string.trim) + |> list.filter(is_not_empty) +} + +/// Create a Command(a) from a Runner(a) +/// +pub fn command(do runner: Runner(a)) -> Command(a) { + Command(do: runner, flags: map.new(), description: "") +} + +/// Attach a description to a Command(a) +/// +pub fn description(cmd: Command(a), description: String) -> Command(a) { + Command(..cmd, description: description) +} + +/// add a `flag.Flag` to a `Command` +/// +pub fn flag( + cmd: Command(a), + at key: String, + of flag: flag.FlagBuilder(_), +) -> Command(a) { + Command(..cmd, flags: map.insert(cmd.flags, key, flag.build(flag))) +} + +/// Add a `flag.Flag to a `Command` when the flag name and builder are bundled as a #(String, flag.FlagBuilder(a)). +/// +/// This is merely a convenience function and calls `glint.flag` under the hood. +/// +pub fn flag_tuple( + cmd: Command(a), + with tup: #(String, flag.FlagBuilder(_)), +) -> Command(a) { + flag(cmd, tup.0, tup.1) +} + +/// Add multiple `Flag`s to a `Command`, note that this function uses `Flag` and not `FlagBuilder(_)`, so the user will need to call `flag.build` before providing the flags here. +/// +/// It is recommended to call `glint.flag` instead. +/// +pub fn flags(cmd: Command(a), with flags: List(#(String, Flag))) -> Command(a) { + use cmd, #(key, flag) <- list.fold(flags, cmd) + Command(..cmd, flags: map.insert(cmd.flags, key, flag)) +} + +/// Add global flags to the existing command tree +/// +pub fn global_flag( + glint: Glint(a), + at key: String, + of flag: flag.FlagBuilder(_), +) -> Glint(a) { + Glint( + ..glint, + global_flags: map.insert(glint.global_flags, key, flag.build(flag)), + ) +} + +/// Add global flags to the existing command tree. +/// +pub fn global_flag_tuple( + glint: Glint(a), + with tup: #(String, flag.FlagBuilder(_)), +) -> Glint(a) { + global_flag(glint, tup.0, tup.1) +} + +/// Add global flags to the existing command tree. +/// +/// Like `glint.flags`, this function requires `Flag`s insead of `FlagBuilder(_)`. +/// +/// It is recommended to use `glint.global_flag` instead. +/// +pub fn global_flags(glint: Glint(a), flags: List(#(String, Flag))) -> Glint(a) { + Glint( + ..glint, + global_flags: { + list.fold( + flags, + glint.global_flags, + fn(acc, tup) { map.insert(acc, tup.0, tup.1) }, + ) + }, + ) +} + +// -- CORE: EXECUTION FUNCTIONS -- + +/// Determines which command to run and executes it. +/// +/// Sets any provided flags if necessary. +/// +/// Each value prefixed with `--` is parsed as a flag. +/// +/// This function does not print its output and is mainly intended for use within `glint` itself. +/// If you would like to print or handle the output of a command please see the `run_and_handle` function. +/// +pub fn execute(glint: Glint(a), args: List(String)) -> CmdResult(a) { + // create help flag to check for + let help_flag = help_flag() + + // check if help flag is present + let #(help, args) = case list.pop(args, fn(s) { s == help_flag }) { + Ok(#(_, args)) -> #(True, args) + _ -> #(False, args) + } + + // split flags out from the args list + let #(flags, args) = list.partition(args, string.starts_with(_, flag.prefix)) + + // search for command and execute + do_execute(glint.cmd, glint.config, glint.global_flags, args, flags, help, []) +} + +/// Find which command to execute and run it with computed flags and args +/// +fn do_execute( + cmd: CommandNode(a), + config: Config, + global_flags: FlagMap, + args: List(String), + flags: List(String), + help: Bool, + command_path: List(String), +) -> CmdResult(a) { + case args { + // when there are no more available arguments + // and help flag has been passed, generate help message + [] if help -> + command_path + |> cmd_help(cmd, config, global_flags) + |> Help + |> Ok + + // when there are no more available arguments + // run the current command + [] -> execute_root(cmd, global_flags, [], flags) + + // when there are arguments remaining + // check if the next one is a subcommand of the current command + [arg, ..rest] -> + case map.get(cmd.subcommands, arg) { + // subcommand found, continue + Ok(cmd) -> + do_execute( + cmd, + config, + global_flags, + rest, + flags, + help, + [arg, ..command_path], + ) + // subcommand not found, but help flag has been passed + // generate and return help message + _ if help -> + command_path + |> cmd_help(cmd, config, global_flags) + |> Help + |> Ok + // subcommand not found, but help flag has not been passed + // execute the current command + _ -> execute_root(cmd, global_flags, args, flags) + } + } +} + +/// Executes the current root command. +/// +fn execute_root( + cmd: CommandNode(a), + global_flags: FlagMap, + args: List(String), + flag_inputs: List(String), +) -> CmdResult(a) { + case cmd.contents { + Some(contents) -> { + use new_flags <- result.try(list.try_fold( + over: flag_inputs, + from: map.merge(global_flags, contents.flags), + with: flag.update_flags, + )) + CommandInput(args, new_flags) + |> contents.do + |> Out + |> Ok + } + None -> snag.error("command not found") + } + |> snag.context("failed to run command") +} + +/// A wrapper for `execute` that prints any errors enountered or the help text if requested. +/// This function ignores any value returned by the command that was run. +/// If you would like to do something with the command output please see the run_and_handle function. +/// +pub fn run(from glint: Glint(a), for args: List(String)) -> Nil { + run_and_handle(from: glint, for: args, with: function.constant(Nil)) +} + +/// A wrapper for `execute` that prints any errors enountered or the help text if requested. +/// This function calls the provided handler with the value returned by the command that was run. +/// +pub fn run_and_handle( + from glint: Glint(a), + for args: List(String), + with handle: fn(a) -> _, +) -> Nil { + case execute(glint, args) { + Error(err) -> + err + |> snag.pretty_print + |> io.println + Ok(Help(help)) -> io.println(help) + Ok(Out(out)) -> { + handle(out) + Nil + } + } +} + +/// Default pretty help heading colouring +/// mint (r: 182, g: 255, b: 234) colour for usage +/// pink (r: 255, g: 175, b: 243) colour for flags +/// buttercup (r: 252, g: 226, b: 174) colour for subcommands +/// +pub fn default_pretty_help() -> PrettyHelp { + let assert Ok(usage_colour) = colour.from_rgb255(182, 255, 234) + let assert Ok(flags_colour) = colour.from_rgb255(255, 175, 243) + let assert Ok(subcommands_colour) = colour.from_rgb255(252, 226, 174) + + PrettyHelp( + usage: usage_colour, + flags: flags_colour, + subcommands: subcommands_colour, + ) +} + +// constants for setting up sections of the help message +const flags_heading = "FLAGS:" + +const subcommands_heading = "SUBCOMMANDS:" + +const usage_heading = "USAGE:" + +/// Helper for filtering out empty strings +/// +fn is_not_empty(s: String) -> Bool { + s != "" +} + +const help_flag_name = "help" + +const help_flag_message = "--help\t\t\tPrint help information" + +/// Function to create the help flag string +/// Exported for testing purposes only +/// +pub fn help_flag() -> String { + flag.prefix <> help_flag_name +} + +// -- HELP: FUNCTIONS -- + +fn wrap_with_space(s: String) -> String { + case s { + "" -> " " + _ -> " " <> s <> " " + } +} + +/// generate the usage help string for a command +fn usage_help(cmd_name: String, flags: FlagMap, config: Config) -> String { + let app_name = option.unwrap(config.name, "gleam run") + let flags = + flags + |> map.to_list + |> list.map(flag.flag_type_help) + |> list.sort(string.compare) + + let flag_sb = case flags { + [] -> sb.new() + _ -> + flags + |> list.intersperse(" ") + |> sb.from_strings() + |> sb.prepend(prefix: " [ ") + |> sb.append(suffix: " ]") + } + + [app_name, wrap_with_space(cmd_name), "[ ARGS ]"] + |> sb.from_strings + |> sb.append_builder(flag_sb) + |> sb.prepend( + config.pretty_help + |> option.map(fn(styling) { heading_style(usage_heading, styling.usage) }) + |> option.unwrap(usage_heading) <> "\n\t", + ) + |> sb.to_string +} + +/// generate the help text for a command +fn cmd_help( + path: List(String), + cmd: CommandNode(a), + config: Config, + global_flags: FlagMap, +) -> String { + // recreate the path of the current command + // reverse the path because it is created by prepending each section as do_execute walks down the tree + let name = + path + |> list.reverse + |> string.join(" ") + + let flags = + option.map(cmd.contents, fn(contents) { contents.flags }) + |> option.lazy_unwrap(map.new) + |> map.merge(global_flags, _) + + let flags_help_body = + config.pretty_help + |> option.map(fn(p) { heading_style(flags_heading, p.flags) }) + |> option.unwrap(flags_heading) <> "\n\t" <> string.join( + list.sort([help_flag_message, ..flag.flags_help(flags)], string.compare), + "\n\t", + ) + + let usage = usage_help(name, flags, config) + + let description = + cmd.contents + |> option.map(fn(contents) { contents.description }) + |> option.unwrap("") + + // create the header block from the name and description + let header_items = + [name, description] + |> list.filter(is_not_empty) + |> string.join("\n") + + // create the subcommands help block + let subcommands = case subcommands_help(cmd.subcommands) { + "" -> "" + subcommands_help_body -> + config.pretty_help + |> option.map(fn(p) { heading_style(subcommands_heading, p.subcommands) }) + |> option.unwrap(subcommands_heading) <> "\n\t" <> subcommands_help_body + } + + // join the resulting help blocks into the final help message + [header_items, usage, flags_help_body, subcommands] + |> list.filter(is_not_empty) + |> string.join("\n\n") +} + +// create the help text for subcommands +fn subcommands_help(cmds: Map(String, CommandNode(a))) -> String { + cmds + |> map.map_values(subcommand_help) + |> map.values + |> list.sort(string.compare) + |> string.join("\n\t") +} + +// generate the help text for a subcommand +fn subcommand_help(name: String, cmd: CommandNode(_)) -> String { + case cmd.contents { + None -> name + Some(contents) -> name <> "\t\t" <> contents.description + } +} + +/// Style heading text with the provided rgb colouring +/// this is only intended for use within glint itself. +/// +fn heading_style(heading: String, colour: Colour) -> String { + heading + |> ansi.bold + |> ansi.underline + |> ansi.italic + |> ansi.hex(colour.to_rgb_hex(colour)) + |> ansi.reset +} + +// -- DEPRECATED: STUBS -- + +/// DEPRECATED: use `glint.cmd` and related new functions instead to create a Command +/// +/// Create command stubs to be used in `add_command_from_stub` +/// +pub type Stub(a) { + Stub( + path: List(String), + run: Runner(a), + flags: List(#(String, Flag)), + description: String, + ) +} + +/// Add a command to the root given a stub +/// +@deprecated("use `glint.cmd` and related new functions instead to create a Command") +pub fn add_command_from_stub(to glint: Glint(a), with stub: Stub(a)) -> Glint(a) { + add( + to: glint, + at: stub.path, + do: command(stub.run) + |> flags(stub.flags) + |> description(stub.description), + ) +} diff --git a/aoc2023/build/packages/glint/src/glint/flag.gleam b/aoc2023/build/packages/glint/src/glint/flag.gleam new file mode 100644 index 0000000..0a6cae1 --- /dev/null +++ b/aoc2023/build/packages/glint/src/glint/flag.gleam @@ -0,0 +1,478 @@ +import gleam/map +import gleam/string +import gleam/result +import gleam/int +import gleam/list +import gleam/float +import snag.{type Result, type Snag} +import gleam/option.{type Option, None, Some} +import glint/flag/constraint.{type Constraint} +import gleam + +/// Flag inputs must start with this prefix +/// +pub const prefix = "--" + +/// The separation character for flag names and their values +const delimiter = "=" + +/// Supported flag types. +/// +pub type Value { + /// Boolean flags, to be passed in as `--flag=true` or `--flag=false`. + /// Can be toggled by omitting the desired value like `--flag`. + /// Toggling will negate the existing value. + /// + B(Internal(Bool)) + + /// Int flags, to be passed in as `--flag=1` + /// + I(Internal(Int)) + + /// List(Int) flags, to be passed in as `--flag=1,2,3` + /// + LI(Internal(List(Int))) + + /// Float flags, to be passed in as `--flag=1.0` + /// + F(Internal(Float)) + + /// List(Float) flags, to be passed in as `--flag=1.0,2.0` + /// + LF(Internal(List(Float))) + + /// String flags, to be passed in as `--flag=hello` + /// + S(Internal(String)) + + /// List(String) flags, to be passed in as `--flag=hello,world` + /// + LS(Internal(List(String))) +} + +/// A type that facilitates the creation of `Flag`s +/// +pub opaque type FlagBuilder(a) { + FlagBuilder( + desc: Description, + parser: Parser(a, Snag), + value: fn(Internal(a)) -> Value, + default: Option(a), + ) +} + +/// An internal representation of flag contents +/// +pub opaque type Internal(a) { + Internal(value: Option(a), parser: Parser(a, Snag)) +} + +// Builder initializers + +type Parser(a, b) = + fn(String) -> gleam.Result(a, b) + +/// initialise an int flag builder +/// +pub fn int() -> FlagBuilder(Int) { + use input <- new(I) + input + |> int.parse + |> result.replace_error(cannot_parse(input, "int")) +} + +/// initialise an int list flag builder +/// +pub fn int_list() -> FlagBuilder(List(Int)) { + use input <- new(LI) + input + |> string.split(",") + |> list.try_map(int.parse) + |> result.replace_error(cannot_parse(input, "int list")) +} + +/// initialise a float flag builder +/// +pub fn float() -> FlagBuilder(Float) { + use input <- new(F) + input + |> float.parse + |> result.replace_error(cannot_parse(input, "float")) +} + +/// initialise a float list flag builder +/// +pub fn float_list() -> FlagBuilder(List(Float)) { + use input <- new(LF) + input + |> string.split(",") + |> list.try_map(float.parse) + |> result.replace_error(cannot_parse(input, "float list")) +} + +/// initialise a string flag builder +/// +pub fn string() -> FlagBuilder(String) { + new(S, fn(s) { Ok(s) }) +} + +/// intitialise a string list flag builder +/// +pub fn string_list() -> FlagBuilder(List(String)) { + use input <- new(LS) + input + |> string.split(",") + |> Ok +} + +/// initialise a bool flag builder +/// +pub fn bool() -> FlagBuilder(Bool) { + use input <- new(B) + case string.lowercase(input) { + "true" | "t" -> Ok(True) + "false" | "f" -> Ok(False) + _ -> Error(cannot_parse(input, "bool")) + } +} + +/// initialize custom builders using a Value constructor and a parsing function +/// +fn new(valuer: fn(Internal(a)) -> Value, p: Parser(a, Snag)) -> FlagBuilder(a) { + FlagBuilder(desc: "", parser: p, value: valuer, default: None) +} + +/// convert a FlagBuilder(a) into its corresponding Flag representation +/// +pub fn build(fb: FlagBuilder(a)) -> Flag { + Flag( + value: fb.value(Internal(value: fb.default, parser: fb.parser)), + description: fb.desc, + ) +} + +/// attach a constraint to a `Flag` +/// +pub fn constraint( + builder: FlagBuilder(a), + constraint: Constraint(a), +) -> FlagBuilder(a) { + FlagBuilder( + ..builder, + parser: wrap_with_constraint(builder.parser, constraint), + ) +} + +/// attach a Constraint(a) to a Parser(a,Snag) +/// this function should not be used directly unless +fn wrap_with_constraint( + p: Parser(a, Snag), + constraint: Constraint(a), +) -> Parser(a, Snag) { + fn(input: String) -> Result(a) { attempt(p(input), constraint) } +} + +fn attempt( + val: gleam.Result(a, e), + f: fn(a) -> gleam.Result(_, e), +) -> gleam.Result(a, e) { + use a <- result.try(val) + result.replace(f(a), a) +} + +/// Flag descriptions +/// +pub type Description = + String + +/// Flag data and descriptions +/// +pub type Flag { + Flag(value: Value, description: Description) +} + +/// attach a description to a `Flag` +/// +pub fn description( + for builder: FlagBuilder(a), + of description: Description, +) -> FlagBuilder(a) { + FlagBuilder(..builder, desc: description) +} + +/// Set the default value for a flag `Value` +/// +pub fn default(for builder: FlagBuilder(a), of default: a) -> FlagBuilder(a) { + FlagBuilder(..builder, default: Some(default)) +} + +/// Associate flag names to their current values. +/// +pub type Map = + map.Map(String, Flag) + +/// Convert a list of flags to a Map. +/// +pub fn build_map(flags: List(#(String, Flag))) -> Map { + map.from_list(flags) +} + +/// Updates a flag value, ensuring that the new value can satisfy the required type. +/// Assumes that all flag inputs passed in start with -- +/// This function is only intended to be used from glint.execute_root +/// +pub fn update_flags(in flags: Map, with flag_input: String) -> Result(Map) { + let flag_input = string.drop_left(flag_input, string.length(prefix)) + + case string.split_once(flag_input, delimiter) { + Ok(data) -> update_flag_value(flags, data) + Error(_) -> attempt_toggle_flag(flags, flag_input) + } +} + +fn update_flag_value(in flags: Map, with data: #(String, String)) -> Result(Map) { + let #(key, input) = data + use contents <- result.try(access(flags, key)) + use value <- result.map( + compute_flag(with: input, given: contents.value) + |> result.map_error(layer_invalid_flag(_, key)), + ) + map.insert(flags, key, Flag(..contents, value: value)) +} + +fn attempt_toggle_flag(in flags: Map, at key: String) -> Result(Map) { + use contents <- result.try(access(flags, key)) + case contents.value { + B(Internal(None, ..) as internal) -> + Internal(..internal, value: Some(True)) + |> B + |> fn(val) { Flag(..contents, value: val) } + |> map.insert(into: flags, for: key) + |> Ok() + B(Internal(Some(val), ..) as internal) -> + Internal(..internal, value: Some(!val)) + |> B + |> fn(val) { Flag(..contents, value: val) } + |> map.insert(into: flags, for: key) + |> Ok() + _ -> Error(no_value_flag_err(key)) + } +} + +fn access_type_error(flag_type) { + snag.error("cannot access flag as " <> flag_type) +} + +fn flag_not_provided_error() { + snag.error("no value provided") +} + +fn construct_value( + input: String, + internal: Internal(a), + constructor: fn(Internal(a)) -> Value, +) -> Result(Value) { + use val <- result.map(internal.parser(input)) + constructor(Internal(..internal, value: Some(val))) +} + +/// Computes the new flag value given the input and the expected flag type +/// +fn compute_flag(with input: String, given current: Value) -> Result(Value) { + input + |> case current { + I(internal) -> construct_value(_, internal, I) + LI(internal) -> construct_value(_, internal, LI) + F(internal) -> construct_value(_, internal, F) + LF(internal) -> construct_value(_, internal, LF) + S(internal) -> construct_value(_, internal, S) + LS(internal) -> construct_value(_, internal, LS) + B(internal) -> construct_value(_, internal, B) + } + |> snag.context("failed to compute value for flag") +} + +// Error creation and manipulation functions +fn layer_invalid_flag(err: Snag, flag: String) -> Snag { + snag.layer(err, "invalid flag '" <> flag <> "'") +} + +fn no_value_flag_err(flag_input: String) -> Snag { + { "flag '" <> flag_input <> "' has no assigned value" } + |> snag.new() + |> layer_invalid_flag(flag_input) +} + +fn undefined_flag_err(key: String) -> Snag { + "flag provided but not defined" + |> snag.new() + |> layer_invalid_flag(key) +} + +fn cannot_parse(with value: String, is kind: String) -> Snag { + { "cannot parse value '" <> value <> "' as " <> kind } + |> snag.new() +} + +// Help Message Functions +/// Generate the help message contents for a single flag +/// +pub fn flag_type_help(flag: #(String, Flag)) { + let #(name, contents) = flag + let kind = case contents.value { + I(_) -> "INT" + B(_) -> "BOOL" + F(_) -> "FLOAT" + LF(_) -> "FLOAT_LIST" + LI(_) -> "INT_LIST" + LS(_) -> "STRING_LIST" + S(_) -> "STRING" + } + + prefix <> name <> delimiter <> "<" <> kind <> ">" +} + +/// Generate help message line for a single flag +/// +fn flag_help(flag: #(String, Flag)) -> String { + flag_type_help(flag) <> "\t\t" <> { flag.1 }.description +} + +/// Generate help messages for all flags +/// +pub fn flags_help(flags: Map) -> List(String) { + flags + |> map.to_list + |> list.map(flag_help) +} + +// -- FLAG ACCESS FUNCTIONS -- + +/// Access the contents for the associated flag +/// +fn access(flags: Map, name: String) -> Result(Flag) { + map.get(flags, name) + |> result.replace_error(undefined_flag_err(name)) +} + +fn get_value( + from flags: Map, + at key: String, + expecting kind: fn(Flag) -> Result(a), +) -> Result(a) { + access(flags, key) + |> result.try(kind) + |> snag.context("failed to retrieve value for flag '" <> key <> "'") +} + +/// Gets the current value for the provided int flag +/// +pub fn get_int_value(from flag: Flag) -> Result(Int) { + case flag.value { + I(Internal(value: Some(val), ..)) -> Ok(val) + I(Internal(value: None, ..)) -> flag_not_provided_error() + _ -> access_type_error("int") + } +} + +/// Gets the current value for the associated int flag +/// +pub fn get_int(from flags: Map, for name: String) -> Result(Int) { + get_value(flags, name, get_int_value) +} + +/// Gets the current value for the provided ints flag +/// +pub fn get_ints_value(from flag: Flag) -> Result(List(Int)) { + case flag.value { + LI(Internal(value: Some(val), ..)) -> Ok(val) + LI(Internal(value: None, ..)) -> flag_not_provided_error() + _ -> access_type_error("int list") + } +} + +/// Gets the current value for the associated ints flag +/// +pub fn get_ints(from flags: Map, for name: String) -> Result(List(Int)) { + get_value(flags, name, get_ints_value) +} + +/// Gets the current value for the provided bool flag +/// +pub fn get_bool_value(from flag: Flag) -> Result(Bool) { + case flag.value { + B(Internal(Some(val), ..)) -> Ok(val) + B(Internal(None, ..)) -> flag_not_provided_error() + _ -> access_type_error("bool") + } +} + +/// Gets the current value for the associated bool flag +/// +pub fn get_bool(from flags: Map, for name: String) -> Result(Bool) { + get_value(flags, name, get_bool_value) +} + +/// Gets the current value for the provided string flag +/// +pub fn get_string_value(from flag: Flag) -> Result(String) { + case flag.value { + S(Internal(value: Some(val), ..)) -> Ok(val) + S(Internal(value: None, ..)) -> flag_not_provided_error() + _ -> access_type_error("string") + } +} + +/// Gets the current value for the associated string flag +/// +pub fn get_string(from flags: Map, for name: String) -> Result(String) { + get_value(flags, name, get_string_value) +} + +/// Gets the current value for the provided strings flag +/// +pub fn get_strings_value(from flag: Flag) -> Result(List(String)) { + case flag.value { + LS(Internal(value: Some(val), ..)) -> Ok(val) + LS(Internal(value: None, ..)) -> flag_not_provided_error() + _ -> access_type_error("string list") + } +} + +/// Gets the current value for the associated strings flag +/// +pub fn get_strings(from flags: Map, for name: String) -> Result(List(String)) { + get_value(flags, name, get_strings_value) +} + +/// Gets the current value for the provided float flag +/// +pub fn get_float_value(from flag: Flag) -> Result(Float) { + case flag.value { + F(Internal(value: Some(val), ..)) -> Ok(val) + F(Internal(value: None, ..)) -> flag_not_provided_error() + _ -> access_type_error("float") + } +} + +/// Gets the current value for the associated float flag +/// +pub fn get_float(from flags: Map, for name: String) -> Result(Float) { + get_value(flags, name, get_float_value) +} + +/// Gets the current value for the provided floats flag +/// +pub fn get_floats_value(from flag: Flag) -> Result(List(Float)) { + case flag.value { + LF(Internal(value: Some(val), ..)) -> Ok(val) + LF(Internal(value: None, ..)) -> flag_not_provided_error() + _ -> access_type_error("float list") + } +} + +/// Gets the current value for the associated floats flag +/// +pub fn get_floats(from flags: Map, for name: String) -> Result(List(Float)) { + get_value(flags, name, get_floats_value) +} diff --git a/aoc2023/build/packages/glint/src/glint/flag/constraint.gleam b/aoc2023/build/packages/glint/src/glint/flag/constraint.gleam new file mode 100644 index 0000000..e474bc2 --- /dev/null +++ b/aoc2023/build/packages/glint/src/glint/flag/constraint.gleam @@ -0,0 +1,66 @@ +import gleam/list +import gleam/result +import gleam/string +import gleam/set +import snag.{type Result} + +/// Constraint type for verifying flag values +/// +pub type Constraint(a) = + fn(a) -> Result(Nil) + +/// one_of returns a Constraint that ensures the parsed flag value is +/// one of the allowed values. +/// +pub fn one_of(allowed: List(a)) -> Constraint(a) { + let allowed_set = set.from_list(allowed) + fn(val: a) -> Result(Nil) { + case set.contains(allowed_set, val) { + True -> Ok(Nil) + False -> + snag.error( + "invalid value '" <> string.inspect(val) <> "', must be one of: [" <> { + allowed + |> list.map(fn(a) { "'" <> string.inspect(a) <> "'" }) + |> string.join(", ") <> "]" + }, + ) + } + } +} + +/// none_of returns a Constraint that ensures the parsed flag value is not one of the disallowed values. +/// +pub fn none_of(disallowed: List(a)) -> Constraint(a) { + let disallowed_set = set.from_list(disallowed) + fn(val: a) -> Result(Nil) { + case set.contains(disallowed_set, val) { + False -> Ok(Nil) + True -> + snag.error( + "invalid value '" <> string.inspect(val) <> "', must not be one of: [" <> { + { + disallowed + |> list.map(fn(a) { "'" <> string.inspect(a) <> "'" }) + |> string.join(", ") <> "]" + } + }, + ) + } + } +} + +/// each is a convenience function for applying a Constraint(a) to a List(a). +/// This is useful because the default behaviour for constraints on lists is that they will apply to the list as a whole. +/// +/// For example, to apply one_of to all items in a `List(Int)`: +/// ```gleam +/// [1, 2, 3, 4] |> one_of |> each +/// ``` +pub fn each(constraint: Constraint(a)) -> Constraint(List(a)) { + fn(l: List(a)) -> Result(Nil) { + l + |> list.try_map(constraint) + |> result.replace(Nil) + } +} diff --git a/aoc2023/build/packages/glint/src/glint@flag.erl b/aoc2023/build/packages/glint/src/glint@flag.erl new file mode 100644 index 0000000..bcce6db --- /dev/null +++ b/aoc2023/build/packages/glint/src/glint@flag.erl @@ -0,0 +1,523 @@ +-module(glint@flag). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([string/0, string_list/0, build/1, constraint/2, description/2, default/2, build_map/1, int/0, int_list/0, float/0, float_list/0, bool/0, flag_type_help/1, flags_help/1, update_flags/2, get_int_value/1, get_int/2, get_ints_value/1, get_ints/2, get_bool_value/1, get_bool/2, get_string_value/1, get_string/2, get_strings_value/1, get_strings/2, get_float_value/1, get_float/2, get_floats_value/1, get_floats/2]). +-export_type([value/0, flag_builder/1, internal/1, flag/0]). + +-type value() :: {b, internal(boolean())} | + {i, internal(integer())} | + {li, internal(list(integer()))} | + {f, internal(float())} | + {lf, internal(list(float()))} | + {s, internal(binary())} | + {ls, internal(list(binary()))}. + +-opaque flag_builder(FTZ) :: {flag_builder, + binary(), + fun((binary()) -> {ok, FTZ} | {error, snag:snag()}), + fun((internal(FTZ)) -> value()), + gleam@option:option(FTZ)}. + +-opaque internal(FUA) :: {internal, + gleam@option:option(FUA), + fun((binary()) -> {ok, FUA} | {error, snag:snag()})}. + +-type flag() :: {flag, value(), binary()}. + +-spec new( + fun((internal(FUR)) -> value()), + fun((binary()) -> {ok, FUR} | {error, snag:snag()}) +) -> flag_builder(FUR). +new(Valuer, P) -> + {flag_builder, <<""/utf8>>, P, Valuer, none}. + +-spec string() -> flag_builder(binary()). +string() -> + new(fun(Field@0) -> {s, Field@0} end, fun(S) -> {ok, S} end). + +-spec string_list() -> flag_builder(list(binary())). +string_list() -> + new(fun(Field@0) -> {ls, Field@0} end, fun(Input) -> _pipe = Input, + _pipe@1 = gleam@string:split(_pipe, <<","/utf8>>), + {ok, _pipe@1} end). + +-spec build(flag_builder(any())) -> flag(). +build(Fb) -> + {flag, + (erlang:element(4, Fb))( + {internal, erlang:element(5, Fb), erlang:element(3, Fb)} + ), + erlang:element(2, Fb)}. + +-spec attempt( + {ok, FVI} | {error, FVJ}, + fun((FVI) -> {ok, any()} | {error, FVJ}) +) -> {ok, FVI} | {error, FVJ}. +attempt(Val, F) -> + gleam@result:'try'(Val, fun(A) -> gleam@result:replace(F(A), A) end). + +-spec wrap_with_constraint( + fun((binary()) -> {ok, FVC} | {error, snag:snag()}), + fun((FVC) -> {ok, nil} | {error, snag:snag()}) +) -> fun((binary()) -> {ok, FVC} | {error, snag:snag()}). +wrap_with_constraint(P, Constraint) -> + fun(Input) -> attempt(P(Input), Constraint) end. + +-spec constraint( + flag_builder(FUY), + fun((FUY) -> {ok, nil} | {error, snag:snag()}) +) -> flag_builder(FUY). +constraint(Builder, Constraint) -> + erlang:setelement( + 3, + Builder, + wrap_with_constraint(erlang:element(3, Builder), Constraint) + ). + +-spec description(flag_builder(FVR), binary()) -> flag_builder(FVR). +description(Builder, Description) -> + erlang:setelement(2, Builder, Description). + +-spec default(flag_builder(FVU), FVU) -> flag_builder(FVU). +default(Builder, Default) -> + erlang:setelement(5, Builder, {some, Default}). + +-spec build_map(list({binary(), flag()})) -> gleam@map:map_(binary(), flag()). +build_map(Flags) -> + gleam@map:from_list(Flags). + +-spec access_type_error(binary()) -> {ok, any()} | {error, snag:snag()}. +access_type_error(Flag_type) -> + snag:error(<<"cannot access flag as "/utf8, Flag_type/binary>>). + +-spec flag_not_provided_error() -> {ok, any()} | {error, snag:snag()}. +flag_not_provided_error() -> + snag:error(<<"no value provided"/utf8>>). + +-spec construct_value(binary(), internal(FWE), fun((internal(FWE)) -> value())) -> {ok, + value()} | + {error, snag:snag()}. +construct_value(Input, Internal, Constructor) -> + gleam@result:map( + (erlang:element(3, Internal))(Input), + fun(Val) -> Constructor(erlang:setelement(2, Internal, {some, Val})) end + ). + +-spec compute_flag(binary(), value()) -> {ok, value()} | {error, snag:snag()}. +compute_flag(Input, Current) -> + _pipe = Input, + _pipe@1 = case Current of + {i, Internal} -> + fun(_capture) -> + construct_value( + _capture, + Internal, + fun(Field@0) -> {i, Field@0} end + ) + end; + + {li, Internal@1} -> + fun(_capture@1) -> + construct_value( + _capture@1, + Internal@1, + fun(Field@0) -> {li, Field@0} end + ) + end; + + {f, Internal@2} -> + fun(_capture@2) -> + construct_value( + _capture@2, + Internal@2, + fun(Field@0) -> {f, Field@0} end + ) + end; + + {lf, Internal@3} -> + fun(_capture@3) -> + construct_value( + _capture@3, + Internal@3, + fun(Field@0) -> {lf, Field@0} end + ) + end; + + {s, Internal@4} -> + fun(_capture@4) -> + construct_value( + _capture@4, + Internal@4, + fun(Field@0) -> {s, Field@0} end + ) + end; + + {ls, Internal@5} -> + fun(_capture@5) -> + construct_value( + _capture@5, + Internal@5, + fun(Field@0) -> {ls, Field@0} end + ) + end; + + {b, Internal@6} -> + fun(_capture@6) -> + construct_value( + _capture@6, + Internal@6, + fun(Field@0) -> {b, Field@0} end + ) + end + end(_pipe), + snag:context(_pipe@1, <<"failed to compute value for flag"/utf8>>). + +-spec layer_invalid_flag(snag:snag(), binary()) -> snag:snag(). +layer_invalid_flag(Err, Flag) -> + snag:layer(Err, <<<<"invalid flag '"/utf8, Flag/binary>>/binary, "'"/utf8>>). + +-spec no_value_flag_err(binary()) -> snag:snag(). +no_value_flag_err(Flag_input) -> + _pipe = (<<<<"flag '"/utf8, Flag_input/binary>>/binary, + "' has no assigned value"/utf8>>), + _pipe@1 = snag:new(_pipe), + layer_invalid_flag(_pipe@1, Flag_input). + +-spec undefined_flag_err(binary()) -> snag:snag(). +undefined_flag_err(Key) -> + _pipe = <<"flag provided but not defined"/utf8>>, + _pipe@1 = snag:new(_pipe), + layer_invalid_flag(_pipe@1, Key). + +-spec cannot_parse(binary(), binary()) -> snag:snag(). +cannot_parse(Value, Kind) -> + _pipe = (<<<<<<"cannot parse value '"/utf8, Value/binary>>/binary, + "' as "/utf8>>/binary, + Kind/binary>>), + snag:new(_pipe). + +-spec int() -> flag_builder(integer()). +int() -> + new(fun(Field@0) -> {i, Field@0} end, fun(Input) -> _pipe = Input, + _pipe@1 = gleam@int:parse(_pipe), + gleam@result:replace_error( + _pipe@1, + cannot_parse(Input, <<"int"/utf8>>) + ) end). + +-spec int_list() -> flag_builder(list(integer())). +int_list() -> + new(fun(Field@0) -> {li, Field@0} end, fun(Input) -> _pipe = Input, + _pipe@1 = gleam@string:split(_pipe, <<","/utf8>>), + _pipe@2 = gleam@list:try_map(_pipe@1, fun gleam@int:parse/1), + gleam@result:replace_error( + _pipe@2, + cannot_parse(Input, <<"int list"/utf8>>) + ) end). + +-spec float() -> flag_builder(float()). +float() -> + new(fun(Field@0) -> {f, Field@0} end, fun(Input) -> _pipe = Input, + _pipe@1 = gleam@float:parse(_pipe), + gleam@result:replace_error( + _pipe@1, + cannot_parse(Input, <<"float"/utf8>>) + ) end). + +-spec float_list() -> flag_builder(list(float())). +float_list() -> + new(fun(Field@0) -> {lf, Field@0} end, fun(Input) -> _pipe = Input, + _pipe@1 = gleam@string:split(_pipe, <<","/utf8>>), + _pipe@2 = gleam@list:try_map(_pipe@1, fun gleam@float:parse/1), + gleam@result:replace_error( + _pipe@2, + cannot_parse(Input, <<"float list"/utf8>>) + ) end). + +-spec bool() -> flag_builder(boolean()). +bool() -> + new( + fun(Field@0) -> {b, Field@0} end, + fun(Input) -> case gleam@string:lowercase(Input) of + <<"true"/utf8>> -> + {ok, true}; + + <<"t"/utf8>> -> + {ok, true}; + + <<"false"/utf8>> -> + {ok, false}; + + <<"f"/utf8>> -> + {ok, false}; + + _ -> + {error, cannot_parse(Input, <<"bool"/utf8>>)} + end end + ). + +-spec flag_type_help({binary(), flag()}) -> binary(). +flag_type_help(Flag) -> + {Name, Contents} = Flag, + Kind = case erlang:element(2, Contents) of + {i, _} -> + <<"INT"/utf8>>; + + {b, _} -> + <<"BOOL"/utf8>>; + + {f, _} -> + <<"FLOAT"/utf8>>; + + {lf, _} -> + <<"FLOAT_LIST"/utf8>>; + + {li, _} -> + <<"INT_LIST"/utf8>>; + + {ls, _} -> + <<"STRING_LIST"/utf8>>; + + {s, _} -> + <<"STRING"/utf8>> + end, + <<<<<<<<<<"--"/utf8, Name/binary>>/binary, "="/utf8>>/binary, "<"/utf8>>/binary, + Kind/binary>>/binary, + ">"/utf8>>. + +-spec flag_help({binary(), flag()}) -> binary(). +flag_help(Flag) -> + <<<<(flag_type_help(Flag))/binary, "\t\t"/utf8>>/binary, + (erlang:element(3, (erlang:element(2, Flag))))/binary>>. + +-spec flags_help(gleam@map:map_(binary(), flag())) -> list(binary()). +flags_help(Flags) -> + _pipe = Flags, + _pipe@1 = gleam@map:to_list(_pipe), + gleam@list:map(_pipe@1, fun flag_help/1). + +-spec access(gleam@map:map_(binary(), flag()), binary()) -> {ok, flag()} | + {error, snag:snag()}. +access(Flags, Name) -> + _pipe = gleam@map:get(Flags, Name), + gleam@result:replace_error(_pipe, undefined_flag_err(Name)). + +-spec update_flag_value(gleam@map:map_(binary(), flag()), {binary(), binary()}) -> {ok, + gleam@map:map_(binary(), flag())} | + {error, snag:snag()}. +update_flag_value(Flags, Data) -> + {Key, Input} = Data, + gleam@result:'try'( + access(Flags, Key), + fun(Contents) -> + gleam@result:map( + begin + _pipe = compute_flag(Input, erlang:element(2, Contents)), + gleam@result:map_error( + _pipe, + fun(_capture) -> layer_invalid_flag(_capture, Key) end + ) + end, + fun(Value) -> + gleam@map:insert( + Flags, + Key, + erlang:setelement(2, Contents, Value) + ) + end + ) + end + ). + +-spec attempt_toggle_flag(gleam@map:map_(binary(), flag()), binary()) -> {ok, + gleam@map:map_(binary(), flag())} | + {error, snag:snag()}. +attempt_toggle_flag(Flags, Key) -> + gleam@result:'try'( + access(Flags, Key), + fun(Contents) -> case erlang:element(2, Contents) of + {b, {internal, none, _} = Internal} -> + _pipe = erlang:setelement(2, Internal, {some, true}), + _pipe@1 = {b, _pipe}, + _pipe@2 = (fun(Val) -> + erlang:setelement(2, Contents, Val) + end)(_pipe@1), + _pipe@3 = gleam@map:insert(Flags, Key, _pipe@2), + {ok, _pipe@3}; + + {b, {internal, {some, Val@1}, _} = Internal@1} -> + _pipe@4 = erlang:setelement( + 2, + Internal@1, + {some, not Val@1} + ), + _pipe@5 = {b, _pipe@4}, + _pipe@6 = (fun(Val@2) -> + erlang:setelement(2, Contents, Val@2) + end)(_pipe@5), + _pipe@7 = gleam@map:insert(Flags, Key, _pipe@6), + {ok, _pipe@7}; + + _ -> + {error, no_value_flag_err(Key)} + end end + ). + +-spec update_flags(gleam@map:map_(binary(), flag()), binary()) -> {ok, + gleam@map:map_(binary(), flag())} | + {error, snag:snag()}. +update_flags(Flags, Flag_input) -> + Flag_input@1 = gleam@string:drop_left( + Flag_input, + gleam@string:length(<<"--"/utf8>>) + ), + case gleam@string:split_once(Flag_input@1, <<"="/utf8>>) of + {ok, Data} -> + update_flag_value(Flags, Data); + + {error, _} -> + attempt_toggle_flag(Flags, Flag_input@1) + end. + +-spec get_value( + gleam@map:map_(binary(), flag()), + binary(), + fun((flag()) -> {ok, FWM} | {error, snag:snag()}) +) -> {ok, FWM} | {error, snag:snag()}. +get_value(Flags, Key, Kind) -> + _pipe = access(Flags, Key), + _pipe@1 = gleam@result:'try'(_pipe, Kind), + snag:context( + _pipe@1, + <<<<"failed to retrieve value for flag '"/utf8, Key/binary>>/binary, + "'"/utf8>> + ). + +-spec get_int_value(flag()) -> {ok, integer()} | {error, snag:snag()}. +get_int_value(Flag) -> + case erlang:element(2, Flag) of + {i, {internal, {some, Val}, _}} -> + {ok, Val}; + + {i, {internal, none, _}} -> + flag_not_provided_error(); + + _ -> + access_type_error(<<"int"/utf8>>) + end. + +-spec get_int(gleam@map:map_(binary(), flag()), binary()) -> {ok, integer()} | + {error, snag:snag()}. +get_int(Flags, Name) -> + get_value(Flags, Name, fun get_int_value/1). + +-spec get_ints_value(flag()) -> {ok, list(integer())} | {error, snag:snag()}. +get_ints_value(Flag) -> + case erlang:element(2, Flag) of + {li, {internal, {some, Val}, _}} -> + {ok, Val}; + + {li, {internal, none, _}} -> + flag_not_provided_error(); + + _ -> + access_type_error(<<"int list"/utf8>>) + end. + +-spec get_ints(gleam@map:map_(binary(), flag()), binary()) -> {ok, + list(integer())} | + {error, snag:snag()}. +get_ints(Flags, Name) -> + get_value(Flags, Name, fun get_ints_value/1). + +-spec get_bool_value(flag()) -> {ok, boolean()} | {error, snag:snag()}. +get_bool_value(Flag) -> + case erlang:element(2, Flag) of + {b, {internal, {some, Val}, _}} -> + {ok, Val}; + + {b, {internal, none, _}} -> + flag_not_provided_error(); + + _ -> + access_type_error(<<"bool"/utf8>>) + end. + +-spec get_bool(gleam@map:map_(binary(), flag()), binary()) -> {ok, boolean()} | + {error, snag:snag()}. +get_bool(Flags, Name) -> + get_value(Flags, Name, fun get_bool_value/1). + +-spec get_string_value(flag()) -> {ok, binary()} | {error, snag:snag()}. +get_string_value(Flag) -> + case erlang:element(2, Flag) of + {s, {internal, {some, Val}, _}} -> + {ok, Val}; + + {s, {internal, none, _}} -> + flag_not_provided_error(); + + _ -> + access_type_error(<<"string"/utf8>>) + end. + +-spec get_string(gleam@map:map_(binary(), flag()), binary()) -> {ok, binary()} | + {error, snag:snag()}. +get_string(Flags, Name) -> + get_value(Flags, Name, fun get_string_value/1). + +-spec get_strings_value(flag()) -> {ok, list(binary())} | {error, snag:snag()}. +get_strings_value(Flag) -> + case erlang:element(2, Flag) of + {ls, {internal, {some, Val}, _}} -> + {ok, Val}; + + {ls, {internal, none, _}} -> + flag_not_provided_error(); + + _ -> + access_type_error(<<"string list"/utf8>>) + end. + +-spec get_strings(gleam@map:map_(binary(), flag()), binary()) -> {ok, + list(binary())} | + {error, snag:snag()}. +get_strings(Flags, Name) -> + get_value(Flags, Name, fun get_strings_value/1). + +-spec get_float_value(flag()) -> {ok, float()} | {error, snag:snag()}. +get_float_value(Flag) -> + case erlang:element(2, Flag) of + {f, {internal, {some, Val}, _}} -> + {ok, Val}; + + {f, {internal, none, _}} -> + flag_not_provided_error(); + + _ -> + access_type_error(<<"float"/utf8>>) + end. + +-spec get_float(gleam@map:map_(binary(), flag()), binary()) -> {ok, float()} | + {error, snag:snag()}. +get_float(Flags, Name) -> + get_value(Flags, Name, fun get_float_value/1). + +-spec get_floats_value(flag()) -> {ok, list(float())} | {error, snag:snag()}. +get_floats_value(Flag) -> + case erlang:element(2, Flag) of + {lf, {internal, {some, Val}, _}} -> + {ok, Val}; + + {lf, {internal, none, _}} -> + flag_not_provided_error(); + + _ -> + access_type_error(<<"float list"/utf8>>) + end. + +-spec get_floats(gleam@map:map_(binary(), flag()), binary()) -> {ok, + list(float())} | + {error, snag:snag()}. +get_floats(Flags, Name) -> + get_value(Flags, Name, fun get_floats_value/1). diff --git a/aoc2023/build/packages/glint/src/glint@flag@constraint.erl b/aoc2023/build/packages/glint/src/glint@flag@constraint.erl new file mode 100644 index 0000000..2978be0 --- /dev/null +++ b/aoc2023/build/packages/glint/src/glint@flag@constraint.erl @@ -0,0 +1,68 @@ +-module(glint@flag@constraint). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]). + +-export([one_of/1, none_of/1, each/1]). + +-spec one_of(list(FSI)) -> fun((FSI) -> {ok, nil} | {error, snag:snag()}). +one_of(Allowed) -> + Allowed_set = gleam@set:from_list(Allowed), + fun(Val) -> case gleam@set:contains(Allowed_set, Val) of + true -> + {ok, nil}; + + false -> + snag:error( + <<<<<<"invalid value '"/utf8, + (gleam@string:inspect(Val))/binary>>/binary, + "', must be one of: ["/utf8>>/binary, + ((<<(begin + _pipe = Allowed, + _pipe@1 = gleam@list:map( + _pipe, + fun(A) -> + <<<<"'"/utf8, + (gleam@string:inspect(A))/binary>>/binary, + "'"/utf8>> + end + ), + gleam@string:join(_pipe@1, <<", "/utf8>>) + end)/binary, + "]"/utf8>>))/binary>> + ) + end end. + +-spec none_of(list(FSL)) -> fun((FSL) -> {ok, nil} | {error, snag:snag()}). +none_of(Disallowed) -> + Disallowed_set = gleam@set:from_list(Disallowed), + fun(Val) -> case gleam@set:contains(Disallowed_set, Val) of + false -> + {ok, nil}; + + true -> + snag:error( + <<<<<<"invalid value '"/utf8, + (gleam@string:inspect(Val))/binary>>/binary, + "', must not be one of: ["/utf8>>/binary, + (((<<(begin + _pipe = Disallowed, + _pipe@1 = gleam@list:map( + _pipe, + fun(A) -> + <<<<"'"/utf8, + (gleam@string:inspect(A))/binary>>/binary, + "'"/utf8>> + end + ), + gleam@string:join(_pipe@1, <<", "/utf8>>) + end)/binary, + "]"/utf8>>)))/binary>> + ) + end end. + +-spec each(fun((FSO) -> {ok, nil} | {error, snag:snag()})) -> fun((list(FSO)) -> {ok, + nil} | + {error, snag:snag()}). +each(Constraint) -> + fun(L) -> _pipe = L, + _pipe@1 = gleam@list:try_map(_pipe, Constraint), + gleam@result:replace(_pipe@1, nil) end. |