From 250f58bcc2b97564fbb897209fa5182f8a5e014f Mon Sep 17 00:00:00 2001 From: Joshua Potter Date: Wed, 13 Dec 2023 12:03:42 -0700 Subject: [PATCH] Add a yes/no prompt type. (#13) --- .clang-format | 2 +- .githooks/pre-commit | 2 +- README.md | 14 +++- include/validator.h | 1 + specs/phoenix/runner | 50 +++++++++---- specs/phoenix/spec.json | 4 + .../template/{README.md => README-ecto.md} | 1 + specs/phoenix/template/README-no-ecto.md | 75 +++++++++++++++++++ src/evaluator.c | 68 +++++++++++++---- src/validator.c | 2 + test/suites.c | 1 + test/test_validator.h | 17 +++++ 12 files changed, 203 insertions(+), 34 deletions(-) rename specs/phoenix/template/{README.md => README-ecto.md} (99%) create mode 100644 specs/phoenix/template/README-no-ecto.md diff --git a/.clang-format b/.clang-format index 65cc883..4340f6d 100644 --- a/.clang-format +++ b/.clang-format @@ -4,5 +4,5 @@ BinPackArguments: false BinPackParameters: false ColumnLimit: 80 ContinuationIndentWidth: 2 -IndentCaseLabels: false +IndentCaseLabels: true IndentWidth: 2 diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 42e68d2..8a0f50e 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -10,7 +10,7 @@ STAGED=$( TARGETS=() while IFS= read -r FILENAME do - if [[ "$FILENAME" =~ .*\.c ]] || [[ "$FILENAME" == .*\.h ]]; then + if [[ "$FILENAME" =~ .*\.c$ ]] || [[ "$FILENAME" == .*\.h$ ]]; then TARGETS+=("${FILENAME}") fi done <<< "$STAGED" diff --git a/README.md b/README.md index ed37b2d..0c4e977 100644 --- a/README.md +++ b/README.md @@ -152,10 +152,18 @@ The value of `type` determines how a field is prompted for. Note the value of `type` is case insenstive. The currently supported list of types are: * `line` - * The simplest prompt type. Takes in a free-form response submitted after - encountering a newline (`\n`). The resulting environment variable has - leading and trailing whitespace trimmed. + * Takes in a free-form response submitted after encountering a newline (`\n`). + The resulting environment variable has leading and trailing whitespace + trimmed. * If `required`, whitespace-only strings are re-prompted. +* `yes` + * Takes in any of `"yes"`, `"y"`, `"no"`, and `"n"`. Answers are case + insensitive. + * Even if not `required`, any answer that does not match one of these patterns + is re-prompted. + * A value of `"yes"` has an environment variable with value `1` passed to the + runner. A value of `"no"` has an environment variable with a null value + (i.e. an empty string) passed to the runner. #### Options diff --git a/include/validator.h b/include/validator.h index dc2fd8a..b6fbf32 100644 --- a/include/validator.h +++ b/include/validator.h @@ -17,6 +17,7 @@ */ enum FieldType { FT_LINE = 1, + FT_YES = 2, }; /** diff --git a/specs/phoenix/runner b/specs/phoenix/runner index d0a0de8..3bd045a 100755 --- a/specs/phoenix/runner +++ b/specs/phoenix/runner @@ -6,7 +6,7 @@ set -e # If set, Bash includes filenames beginning with a `.` in the results of # filename expansion. The filenames `.` and `..` must always be matched # explicitly, even if dotglob is set. -shopt -s dotglob +shopt -s dotglob extglob # ============================================================ # PROLOGUE @@ -45,12 +45,18 @@ else MODULE_ARG="--module '$MODULE'" fi +if [ -n "$ECTO" ]; then + ECTO_ARG= +else + ECTO_ARG="--no-ecto" +fi + # ============================================================ # BUILD # ============================================================ # Copy template contents over to the intermediate build directory. -cp -r template/* "$BUILD" +cp -r template/!(README*) "$BUILD" # Explicitly set permissions on all copied template files. find "$BUILD" -type f -execdir chmod 644 {} + @@ -61,14 +67,18 @@ chmod 755 "$BUILD"/.githooks/pre-commit # subdirectory to avoid interactive conflict resolution. nix develop "$BUILD" \ --command bash \ - -c "mix phx.new $BUILD/bs.project --app '$APP' $MODULE_ARG" + -c "mix phx.new $BUILD/bs.project --app '$APP' $MODULE_ARG $ECTO_ARG" # Copy the generated files into the intermediate build directory. mv -f "$BUILD"/bs.project/* "$BUILD" rmdir "$BUILD"/bs.project # Overwrite the generated README in favor of that defined in our template. -cp template/README.md "$BUILD" +if [ -n "$ECTO" ]; then + cp template/README-ecto.md "$BUILD"/README.md +else + cp template/README-no-ecto.md "$BUILD"/README.md +fi chmod 644 "$BUILD"/README.md # Include additional build files for our assets. Group into subdirectory so @@ -80,7 +90,9 @@ mv "$BUILD"/bs.assets/* "$BUILD"/assets rmdir "$BUILD"/bs.assets # Create a new database cluster. -nix develop "$BUILD" --command bash -c "pg_ctl initdb -D $BUILD/db" +if [ -n "$ECTO" ]; then + nix develop "$BUILD" --command bash -c "pg_ctl initdb -D $BUILD/db" +fi # ============================================================ # REWRITES @@ -98,14 +110,15 @@ done # Typically when building a Phoenix application, Mix will download # esbuild/tailwind binaries on demand. Within nix environments this is not # possible. Instead, specify them directly with these environment variables. -sed -i \ - '42 s/$/,/; - 43 i \ \ path: System.get_env("MIX_ESBUILD_PATH") - s/version: \"0.17.11\"/version: \"0.19.7\"/ - 54 s/$/,/; - 55 i \ \ path: System.get_env("MIX_TAILWIND_PATH") - s/version: \"3.3.2\"/version: \"3.3.5\"/' \ - "$BUILD/config/config.exs" +sed -i '/config :esbuild/, /^$/ { + s/0.17.11/0.19.7/; s/]$/],/; + s/^$/\ \ path: System.get_env("MIX_ESBUILD_PATH")\n/}' \ + "$BUILD"/config/config.exs + +sed -i '/config :tailwind/, /^$/ { + s/3.3.2/3.3.5/; s/]$/],/; + s/^$/\ \ path: System.get_env("MIX_TAILWIND_PATH")\n/}' \ + "$BUILD"/config/config.exs # By default Phoenix generates a postgres configuration with assumed username # `postgres`. This flake encourages a local postgres database with username @@ -114,9 +127,6 @@ sed -i "s/username: \"postgres\"/username: \"$(whoami)\"/g" "$BUILD/config/dev.e # Append an additional rule to `.gitignore` to ignore the database cluster. cat <> "$BUILD"/.gitignore -# The default location of the generated database cluster. -/db/ - # Directory used by \`direnv\` to hold \`use flake\`-generated profiles. /.direnv/ @@ -124,6 +134,14 @@ cat <> "$BUILD"/.gitignore /result EOF +if [ -n "$ECTO" ]; then + cat <> "$BUILD"/.gitignore + +# The default location of the generated database cluster. +/db/ +EOF +fi + # ============================================================ # EPILOGUE # ============================================================ diff --git a/specs/phoenix/spec.json b/specs/phoenix/spec.json index d8f64ed..da5af0f 100644 --- a/specs/phoenix/spec.json +++ b/specs/phoenix/spec.json @@ -7,5 +7,9 @@ "type": "line", "required": false, "prompt": "Module> " + }, + "ecto": { + "type": "yes", + "prompt": "Include Ecto? [Yn] " } } diff --git a/specs/phoenix/template/README.md b/specs/phoenix/template/README-ecto.md similarity index 99% rename from specs/phoenix/template/README.md rename to specs/phoenix/template/README-ecto.md index 0a606f0..16d99a1 100644 --- a/specs/phoenix/template/README.md +++ b/specs/phoenix/template/README-ecto.md @@ -28,6 +28,7 @@ $ pg_ctl -D db stop Afterward, you can run the Phoenix setup commands: ```bash +$ mix deps.get $ mix ecto.setup $ mix assets.setup ``` diff --git a/specs/phoenix/template/README-no-ecto.md b/specs/phoenix/template/README-no-ecto.md new file mode 100644 index 0000000..d6a57bf --- /dev/null +++ b/specs/phoenix/template/README-no-ecto.md @@ -0,0 +1,75 @@ +# Phoenix Flake Template + +This is a template for constructing a environment for Elixir development +(version 1.15.7, Erlang/OTP 25) with the [Phoenix](https://www.phoenixframework.org/) +(version 1.7.10) framework. [direnv](https://direnv.net/) can be used to launch +a dev shell upon entering this directory (refer to `.envrc`). Otherwise run via: +```bash +$ nix develop +``` + +## Quickstart + +Run the Phoenix setup command and then start the local server: +```bash +$ mix deps.get +$ mix assets.setup +$ mix phx.server +``` + +## Dependencies + +### Backend + +Mix dependencies are packaged using [mix2nix](https://github.com/ydlr/mix2nix). +After updating your `mix.lock` file, make sure to re-run the following: +```bash +$ mix2nix > deps.nix +``` +As of now, `mix2nix` cannot handle git dependencies found inside the `mix.lock` +file. If you have git dependencies, add them manually or use +[FODs](https://nixos.org/manual/nixpkgs/stable/#packaging-beam-applications). + +### Frontend + +Frontend dependencies (i.e. assets found in the `/assets` folder) are packaged +using [node2nix](https://github.com/svanderburg/node2nix). You can generate the +relevant nix files for import using the following sequence of commands: +```bash +$ cd assets +$ rm -r node_modules # If this directory exists. +$ node2nix -l +``` +In the above, we must remove `node_modules` (if it exists). Otherwise the +node packages will be included in the Nix build, influencing the outcome of +`node2nix`. The above generates three files: + +* `node-packages.nix` + * Captures the packages that can be deployed (including all its required + dependencies) +* `node-env.nix` + * Contains build logic +* `default.nix` + * A composition expression allowing users to deploy the package. For an + example of this deployment, refer to `flake.nix` + +NOTE: Do not update the lock version used in `assets`. `node2nix` currently only +supports lock versions 1 and 2. + +## Language Server + +The [elixir-ls](https://github.com/elixir-lsp/elixir-ls) LSP (version 0.17.10) +and [typescript-language-server](https://github.com/typescript-language-server/typescript-language-server) +(version 4.1.2) is included in this flake. + +## Formatting + +Formatting depends on [prettier](https://prettier.io/) (version 3.1.0) and the +`mix format` task. A `pre-commit` hook is included in `.githooks` that can be +used to format all `*.exs?`, `*.jsx?`, and `*.tsx?` files prior to commit. +Install via: +```bash +$ git config --local core.hooksPath .githooks/ +``` +If running [direnv](https://direnv.net/), this hook is installed automatically +when entering the directory. diff --git a/src/evaluator.c b/src/evaluator.c index 7a7b3ab..6203644 100644 --- a/src/evaluator.c +++ b/src/evaluator.c @@ -79,7 +79,7 @@ static void print_prompt(const struct Field *const field) { } } -static const char *query_line(const struct Field *const field) { +static char *query_line(const struct Field *const field) { assert(field); // TODO: Dynamically size this value. @@ -103,6 +103,38 @@ static const char *query_line(const struct Field *const field) { return 0; } +static bool query_yes(const struct Field *const field) { + assert(field); + + // TODO: Dynamically size this value. + char *response = calloc(1, 1024); + bool answer = false; + + while (true) { + print_prompt(field); + if (fgets(response, 1024, stdin)) { + trim_leading(response); + trim_trailing(response); + if (strcmp_ci(response, "y") == 0 || strcmp_ci(response, "yes") == 0) { + answer = true; + break; + } else if ( + strcmp_ci(response, "n") == 0 || + strcmp_ci(response, "no") == 0 || + (response[0] == 0 && !field->required) + ) { + break; + } + } else { // Likely EOF. Force-quit even if required. + printf("\n"); + break; + } + } + + free(response); + return answer; +} + static void push_env( struct StringBuf *env, const char *key, const char *value ) { @@ -129,20 +161,30 @@ static struct Error *push_fields( ) { for (int i = 0; i < fields->size; ++i) { struct Field *field = fields->buf[i]; - const char *response = 0; switch (field->type) { - case FT_LINE: - response = query_line(field); - break; + case FT_LINE: { + char *response = query_line(field); + if (field->required && !response) { + return ERROR_NEW( + ERROR_EVALUATOR_RESPONSE_INVALID, + ANSI_RED_F("ERROR"), + ": Could not read response." + ); + } + push_env(*env_buf, field->key, response); + free(response); + break; + } + case FT_YES: { + bool response = query_yes(field); + if (response) { + push_env(*env_buf, field->key, "1"); + } else { + push_env(*env_buf, field->key, ""); + } + break; + } } - if (field->required && !response) { - return ERROR_NEW( - ERROR_EVALUATOR_RESPONSE_INVALID, - ANSI_RED_F("ERROR"), - ": Could not read response." - ); - } - push_env(*env_buf, field->key, response); } return 0; } diff --git a/src/validator.c b/src/validator.c index 4b3d7e3..a719066 100644 --- a/src/validator.c +++ b/src/validator.c @@ -70,6 +70,8 @@ static struct Error *read_field( if (strcmp_ci(type->valuestring, "line") == 0) { (*out)->type = FT_LINE; + } else if (strcmp_ci(type->valuestring, "yes") == 0) { + (*out)->type = FT_YES; } else { error = ERROR_NEW( ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN, diff --git a/test/suites.c b/test/suites.c index f4fdcf6..701c13b 100644 --- a/test/suites.c +++ b/test/suites.c @@ -47,6 +47,7 @@ int main(int argc, char *argv[]) { sput_run_test(test_validator_field_required_valid); sput_run_test(test_validator_field_prompt_invalid); sput_run_test(test_validator_valid_no_required); + sput_run_test(test_validator_field_type_yes); sput_finish_testing(); diff --git a/test/test_validator.h b/test/test_validator.h index 4b3248e..7ba89e6 100644 --- a/test/test_validator.h +++ b/test/test_validator.h @@ -240,4 +240,21 @@ static void test_validator_valid_no_required() { test_validator_teardown(fixture); } +static void test_validator_field_type_yes() { + struct TestValidatorFixture *fixture = test_validator_setup( + "{" + " \"abc\": {" + " \"type\": \"yes\"" + " \"prompt\": \"What value for key?\"" + " }" + "}" + ); + + struct Error *error = + validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts); + sput_fail_unless(error == 0, "yes valid"); + + test_validator_teardown(fixture); +} + #endif /* _BOOTSTRAP_TEST_VALIDATOR */