Add a yes/no prompt type. (#13)

pull/1/head
Joshua Potter 2023-12-13 12:03:42 -07:00 committed by GitHub
parent 6fb8b6c516
commit 250f58bcc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 203 additions and 34 deletions

View File

@ -4,5 +4,5 @@ BinPackArguments: false
BinPackParameters: false
ColumnLimit: 80
ContinuationIndentWidth: 2
IndentCaseLabels: false
IndentCaseLabels: true
IndentWidth: 2

View File

@ -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"

View File

@ -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

View File

@ -17,6 +17,7 @@
*/
enum FieldType {
FT_LINE = 1,
FT_YES = 2,
};
/**

View File

@ -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 <<EOF >> "$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 <<EOF >> "$BUILD"/.gitignore
/result
EOF
if [ -n "$ECTO" ]; then
cat <<EOF >> "$BUILD"/.gitignore
# The default location of the generated database cluster.
/db/
EOF
fi
# ============================================================
# EPILOGUE
# ============================================================

View File

@ -7,5 +7,9 @@
"type": "line",
"required": false,
"prompt": "Module> "
},
"ecto": {
"type": "yes",
"prompt": "Include Ecto? [Yn] "
}
}

View File

@ -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
```

View File

@ -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.

View File

@ -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;
}

View File

@ -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,

View File

@ -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();

View File

@ -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 */