Compare commits

...

102 commits

Author SHA1 Message Date
a87393edda chore(deps): update dependency ash to v3.4.12
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-17 01:42:26 +12:00
5886186586 chore(deps): update dependency spark to v2.2.29
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-16 12:21:41 +12:00
a51e9e7a02 chore: fix failing test.
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-15 19:31:33 +12:00
6c4a3c0f8a chore(deps): update dependency ash to v3.4.11
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2024-09-15 10:25:37 +12:00
15b072db81 chore(deps): update dependency spark to v2.2.25
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-11 00:23:56 +12:00
141978c5be
chore: unlock unused dependencies.
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-10 11:17:54 +12:00
8cdef5d967 chore(deps): update dependency ash to v3.4.8
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2024-09-10 03:18:30 +12:00
a750c4a7ff chore(deps): update dependency ash to v3.4.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-07 02:23:32 +12:00
e77bd9de2a chore(deps): update dependency ash to v3.4.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-06 06:21:51 +12:00
39bd04b4af chore(deps): update dependency ash to v3.4.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-05 15:21:33 +12:00
13c5619376 chore(deps): update dependency ash to v3.4.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-05 03:19:43 +12:00
585766dd43 chore(deps): update dependency ash to v3.4.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-04 15:19:42 +12:00
3e18763805 chore(deps): update dependency spark to v2.2.23
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-30 10:20:57 +12:00
5e3aa634ec chore(deps): update dependency spark to v2.2.22
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-08-20 14:36:09 +12:00
90aad764f5
chore: unlock unused deps.
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-17 20:16:44 +12:00
b862125133
chore(deps): update dependency ash to v3.4.1 2024-08-17 20:16:34 +12:00
275110ba13
chore: fix failing test from Spark update
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-17 19:45:09 +12:00
ffc5407b2c chore(deps): update dependency spark to v2.2.21
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
renovate/artifacts Artifact file update failure
2024-08-15 22:11:29 +12:00
64d17a44c5 chore(deps): update dependency spark to v2.2.19
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-14 13:15:51 +12:00
3f17cd9e99 chore(deps): update dependency ash to v3.3.3
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-08 16:32:27 +12:00
3a0ddd207e chore(deps): update dependency spark to v2.2.11
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-08 16:25:37 +12:00
324e15ca46 chore(deps): update dependency ash to v3.3.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-28 09:29:45 +12:00
43f5360130
chore: remove unused dependencies.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-07-27 15:33:03 +12:00
43535e1d28 chore(deps): update dependency ash to v3.2.6
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2024-07-27 06:36:30 +12:00
3bbc2d62a3 chore(deps): update dependency mix_audit to v2.1.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-17 02:32:00 +12:00
975f7335e5 chore(deps): update dependency ash to v3.1.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-15 06:35:47 +12:00
e11660011e chore(deps): update dependency ash to v3.1.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-15 03:39:47 +12:00
01962eb4c9 chore(deps): update dependency ash to v3.1.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-15 01:36:14 +12:00
a217a0fe11 chore(deps): update dependency ash to v3.1.4
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2024-07-14 08:36:44 +12:00
60be63d5d2 chore(deps): update dependency earmark to v1.4.47
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-13 06:39:02 +12:00
8c4e71614d chore(deps): update dependency ash to v3.1.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-12 03:41:39 +12:00
e6df6910c7 chore(deps): update dependency ash to v3.1.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-11 04:35:20 +12:00
f767b723e0 chore(deps): update dependency erlang to v27.0.1
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-11 02:55:57 +12:00
df1f1309e4 chore(deps): update dependency ash to v3.1.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-11 01:39:42 +12:00
7b041d3055 chore(deps): update dependency ash to v3.1.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-10 12:35:00 +12:00
334363db70 chore(deps): update dependency ex_doc to v0.34.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-09 03:43:11 +12:00
5d5edad3e5 chore(deps): update dependency elixir to v1.17.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-07 09:38:21 +12:00
e60facc798 docs: fix typo in related docs (#110)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #110
Co-authored-by: James Harton <james@harton.nz>
Co-committed-by: James Harton <james@harton.nz>
2024-07-04 10:29:21 +12:00
8cc0de70c4 chore(deps): update dependency spark to v2.2.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-03 10:35:55 +12:00
3eb46cb485 chore(deps): update dependency spark to v2.2.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-29 10:39:44 +12:00
452f827067 chore(deps): update dependency spark to v2.2.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-26 07:37:37 +12:00
e590e8d9e8 chore(deps): update dependency recase to v0.8.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-25 18:37:11 +12:00
e074b261d2 chore(deps): update dependency ash to v3.0.16
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-22 07:18:53 +12:00
fbabaa6c93 chore(deps): update dependency spark to v2.2.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-21 06:20:17 +12:00
59ea39c8ab chore(deps): update dependency ex_doc to v0.34.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-21 02:21:25 +12:00
e18319f525 chore(deps): update dependency elixir to v1.17.1
Some checks reported errors
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build encountered an error
2024-06-19 12:23:17 +12:00
957e2845c7 chore(deps): update dependency ash to v3.0.15
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-19 11:23:38 +12:00
00cdf8106d chore(deps): update dependency ash to v3.0.14
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-19 04:21:51 +12:00
d756b1ac12 chore(deps): update dependency spark to v2.2.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-18 10:21:11 +12:00
e8fb1bb305 chore(deps): update dependency spark to v2.2.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-18 06:19:26 +12:00
8dd043f13a chore(deps): update dependency ash to v3.0.13
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-18 04:18:45 +12:00
9a26e7a74b chore(deps): update dependency elixir to v1.17.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-15 13:43:13 +12:00
ccbe9a124f chore(deps): update dependency credo to v1.7.7
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-15 13:27:13 +12:00
5d767a2e91 chore(deps): update dependency ash to v3.0.12
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-15 12:52:01 +12:00
13bd3c81b7 chore(deps): update dependency ash to v3.0.11
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-12 03:22:20 +12:00
a874167d10 chore(deps): update dependency spark to v2.1.24
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-11 03:19:53 +12:00
81b312b172 chore(deps): update dependency spark to v2.1.23
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-08 02:22:07 +12:00
f9dc1112f0 chore(deps): update dependency ash to v3.0.10
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-07 06:33:11 +12:00
c5adaf448b chore(deps): update dependency ash to v3.0.9
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-01 03:18:37 +12:00
85aa3e2f6e chore: release version v0.9.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-05-30 20:54:44 +00:00
f8480ec8e7 fix: allow auto builds to be replaced by explicit relate commands.
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-31 08:54:03 +12:00
Rebecca Le
d5be51556d bug: Add failing test for auto-build and relate options colliding 2024-05-31 08:54:03 +12:00
ebc07800b4 chore(deps): update dependency ex_doc to v0.34.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-31 01:24:03 +12:00
5368a030dd chore: release version v0.9.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-05-28 21:51:27 +00:00
ef5d6462b9 feat: Auto build/load factory options. (#83)
All checks were successful
continuous-integration/drone/push Build is passing
New factory DSL options:

1. `auto_build` allows you to provide a list of relationships which must also be built when building that factory.
2. `auto_load` allows you to provide a load statement for relationships and calculations that must be loaded when building that factory.

Reviewed-on: #83
Co-authored-by: James Harton <james@harton.nz>
Co-committed-by: James Harton <james@harton.nz>
2024-05-29 09:50:43 +12:00
f46d9bb6b9 chore(deps): update dependency ash to v3.0.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-29 07:19:14 +12:00
4786ee97e6 chore(deps): update dependency recase to ~> 0.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-28 21:22:06 +12:00
0d42238d6f chore: release version v0.8.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-05-28 04:48:48 +00:00
cb2d0376b5
fix: Include :variant in option schema.
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
This required a bit of a rework of how the options are validated.  Now they're only validated once when `Builder.build` is called instead of inside each builder.
2024-05-28 16:40:43 +12:00
aa65f4912b
fix: bug with generator arguments. 2024-05-28 16:33:00 +12:00
5c524faef8 chore: release version v0.8.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-05-26 08:51:36 +00:00
5f8e19ee07
feat: Add before_build and after_build entities to factories.
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-26 20:50:33 +12:00
56e6fb9a4c chore(deps): update dependency ash to v3.0.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-24 18:18:27 +12:00
04f1393c7c chore(deps): update dependency ash to v3.0.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-24 11:19:07 +12:00
bab42763d3 chore(deps): update dependency ash to v3.0.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-24 03:17:37 +12:00
1ed4612aec chore(deps): update dependency ash to v3.0.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-23 11:19:09 +12:00
9a5579bb50 chore(deps): update dependency ash to v3.0.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-23 10:19:03 +12:00
f03b4487bf chore(deps): update dependency ex_doc to v0.33.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-22 01:20:12 +12:00
535acaa1a2 chore(deps): update dependency elixir to v1.16.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-21 11:21:16 +12:00
550178ed34 chore(deps): update dependency erlang to v27
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-20 21:42:45 +12:00
690a388a93 chore: release version v0.7.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-05-20 04:59:12 +00:00
e99dec4808
feat(related): Newly build records can now be related to existing records.
All checks were successful
continuous-integration/drone/push Build is passing
Closes #14.
2024-05-20 16:57:18 +12:00
0139a0e496
chore: re-enable auto releasing now that Ash 3.0 is out. 2024-05-20 16:56:17 +12:00
0bf111b22a chore(deps): update dependency ash to v3.0.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-16 10:18:27 +12:00
50eb88866f chore(deps): update dependency spark to v2.1.22
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-15 04:17:53 +12:00
d5acff239d chore(deps): update dependency ash to v3.0.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-15 02:35:09 +12:00
198cb16a9c
chore: release version v0.6.2
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-05-11 15:28:05 +12:00
13395f343c chore(deps): update dependency spark to v2.1.21
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-11 11:26:40 +12:00
f6d6529f88 chore(deps): update dependency git_ops to v2.6.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-11 10:39:28 +12:00
35e0d80dbd chore(deps): update dependency ex_doc to v0.32.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-10 23:37:48 +12:00
51960bb6b6 chore(deps): update dependency credo to v1.7.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-09 23:58:22 +12:00
29eb9d2a5b chore(deps): update dependency erlang to v26.2.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-03 03:35:39 +12:00
e2b88e262e chore(deps): update dependency spark to v2.1.20
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-22 05:30:50 +12:00
7d294a5e25 chore(deps): update dependency spark to v2.1.19
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-22 03:33:35 +12:00
b9039bd529 chore(deps): update dependency ex_doc to v0.32.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-13 08:34:54 +12:00
2c25a1eda6 chore(deps): update dependency spark to v2.1.18
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-13 06:31:31 +12:00
edfe181d7d chore(deps): update dependency erlang to v26.2.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-13 00:35:51 +12:00
9ad6337ac3 chore(deps): update dependency spark to v2.1.17
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-12 05:30:12 +12:00
dd68439047 chore(deps): update dependency spark to v2.1.15
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-11 04:58:15 +12:00
25c67edccf chore(deps): update dependency spark to v2.1.14
Some checks reported errors
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build encountered an error
2024-04-11 02:30:09 +12:00
492e90badf chore(deps): update dependency ex_doc to v0.32.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-10 21:32:20 +12:00
0e895100ea chore(deps): update dependency spark to v2.1.13
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-06 12:30:56 +13:00
30 changed files with 945 additions and 182 deletions

View file

@ -252,47 +252,47 @@ steps:
- git log -1 --format=%s > .last_commit_message - git log -1 --format=%s > .last_commit_message
- asdf mix git_ops.check_message .last_commit_message - asdf mix git_ops.check_message .last_commit_message
# - name: mix git_ops.release - name: mix git_ops.release
# image: harton.dev/james/asdf_container:latest image: harton.dev/james/asdf_container:latest
# when: when:
# branch: branch:
# - main - main
# event: event:
# exclude: exclude:
# - pull_request - pull_request
# depends_on: depends_on:
# - mix test - mix test
# - mix credo - mix credo
# - mix hex.audit - mix hex.audit
# - mix format - mix format
# - mix spark.formatter - mix spark.formatter
# - mix spark.cheat_sheets - mix spark.cheat_sheets
# - mix deps.unlock - mix deps.unlock
# - mix doctor - mix doctor
# - mix git_ops.check_message - mix git_ops.check_message
# environment: environment:
# MIX_ENV: test MIX_ENV: test
# HEX_HOME: /drone/src/.hex HEX_HOME: /drone/src/.hex
# MIX_HOME: /drone/src/.mix MIX_HOME: /drone/src/.mix
# REBAR_BASE_DIR: /drone/src/.rebar3 REBAR_BASE_DIR: /drone/src/.rebar3
# ASDF_DATA_DIR: /drone/src/.asdf ASDF_DATA_DIR: /drone/src/.asdf
# ASDF_DIR: /root/.asdf ASDF_DIR: /root/.asdf
# DRONE_TOKEN: DRONE_TOKEN:
# from_secret: DRONE_TOKEN from_secret: DRONE_TOKEN
# commands: commands:
# - git fetch --tags - git fetch --tags
# - . $ASDF_DIR/asdf.sh - . $ASDF_DIR/asdf.sh
# - mix git_ops.project_info --format=shell > before.env - mix git_ops.project_info --format=shell > before.env
# - mix git_ops.release --yes --no-major || true - mix git_ops.release --yes --no-major || true
# - mix git_ops.project_info --format=shell > after.env - mix git_ops.project_info --format=shell > after.env
# - . ./before.env - . ./before.env
# - export OLD_APP_VERSION=$${APP_VERSION} - export OLD_APP_VERSION=$${APP_VERSION}
# - . ./after.env - . ./after.env
# - export NEW_APP_VERSION=$${APP_VERSION} - export NEW_APP_VERSION=$${APP_VERSION}
# - if [ "v$${OLD_APP_VERSION}" != "v$${NEW_APP_VERSION}" ]; then - if [ "v$${OLD_APP_VERSION}" != "v$${NEW_APP_VERSION}" ]; then
# - export GIT_URL=$(echo $DRONE_GIT_HTTP_URL | sed -e "s/:\\/\\//:\\/\\/$DRONE_REPO_OWNER:$DRONE_TOKEN@/") - export GIT_URL=$(echo $DRONE_GIT_HTTP_URL | sed -e "s/:\\/\\//:\\/\\/$DRONE_REPO_OWNER:$DRONE_TOKEN@/")
# - git push $${GIT_URL} "HEAD:${DRONE_COMMIT_REF}" "refs/tags/v$${NEW_APP_VERSION}" - git push $${GIT_URL} "HEAD:${DRONE_COMMIT_REF}" "refs/tags/v$${NEW_APP_VERSION}"
# - fi - fi
- name: build artifacts - name: build artifacts
image: harton.dev/james/asdf_container:latest image: harton.dev/james/asdf_container:latest

View file

@ -1,6 +1,12 @@
spark_locals_without_parens = [ spark_locals_without_parens = [
after_build: 1,
after_build: 2,
attribute: 2, attribute: 2,
attribute: 3, attribute: 3,
auto_build: 1,
auto_load: 1,
before_build: 1,
before_build: 2,
domain: 1, domain: 1,
factory: 1, factory: 1,
factory: 2, factory: 2,

View file

@ -1,2 +1,2 @@
elixir 1.16.2-otp-26 elixir 1.17.2
erlang 26.2.3 erlang 27.0.1

8
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"cSpell.words": [
"arities",
"Cardassia",
"recase",
"Slickback"
]
}

View file

@ -5,6 +5,58 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
<!-- changelog --> <!-- changelog -->
## [v0.9.1](https://harton.dev/james/smokestack/compare/v0.9.0...v0.9.1) (2024-05-30)
### Bug Fixes:
* allow auto builds to be replaced by explicit relate commands.
## [v0.9.0](https://harton.dev/james/smokestack/compare/v0.8.1...v0.9.0) (2024-05-28)
### Features:
* Auto build/load factory options. (#83)
## [v0.8.1](https://harton.dev/james/smokestack/compare/v0.8.0...v0.8.1) (2024-05-28)
### Bug Fixes:
* Include `:variant` in option schema.
* bug with generator arguments.
## [v0.8.0](https://harton.dev/james/smokestack/compare/v0.7.0...v0.8.0) (2024-05-26)
### Features:
* Add `before_build` and `after_build` entities to factories.
## [v0.7.0](https://harton.dev/james/smokestack/compare/v0.6.2...v0.7.0) (2024-05-20)
### Features:
* related: Newly build records can now be related to existing records.
## [v0.6.2](https://harton.dev/james/smokestack/compare/v0.6.1-rc.2...v0.6.2) (2024-05-11)
## [v0.6.1-rc.2](https://harton.dev/james/smokestack/compare/v0.6.1-rc.1...v0.6.1-rc.2) (2024-04-03) ## [v0.6.1-rc.2](https://harton.dev/james/smokestack/compare/v0.6.1-rc.1...v0.6.1-rc.2) (2024-04-03)

View file

@ -38,7 +38,7 @@ add it directly to your `mix.exs`:
```elixir ```elixir
def deps do def deps do
[ [
{:smokestack, "~> 0.6.1-rc.2"}, {:smokestack, "~> 0.9.1"},
] ]
end end
``` ```

View file

@ -13,7 +13,9 @@ The DSL definition for the Smokestack DSL.
* smokestack * smokestack
* factory * factory
* after_build
* attribute * attribute
* before_build
### Docs ### Docs
@ -22,7 +24,9 @@ The DSL definition for the Smokestack DSL.
* [factory](#module-factory) * [factory](#module-factory)
* after_build
* attribute * attribute
* before_build
@ -38,7 +42,9 @@ The DSL definition for the Smokestack DSL.
Define factories for a resource Define factories for a resource
* after_build
* attribute * attribute
* before_build
@ -48,6 +54,30 @@ Define factories for a resource
* `:variant` (`t:atom/0`) - The name of a factory variant The default value is `:default`. * `:variant` (`t:atom/0`) - The name of a factory variant The default value is `:default`.
* `:auto_build` (one or a list of `t:atom/0`) - A list of relationships that should always be built when building this factory The default value is `[]`.
* `:auto_load` - An Ash "load statement" to always apply when building this factory The default value is `[]`.
##### after_build
Modify the record after building.
Allows you to provide a function which can modify the built record before returning.
These hooks are only applied when building records and not parameters.
* `:hook` (mfa or function of arity 1) - Required. A function which returns an updated record
##### attribute ##### attribute
@ -67,6 +97,24 @@ Define factories for a resource
##### before_build
Modify the attributes before building.
Allows you to provide a function which can modify the the attributes before building.
* `:hook` (mfa or function of arity 1) - Required. A function which returns an updated record
@ -79,7 +127,9 @@ Define factories for a resource
### Nested DSLs ### Nested DSLs
* [factory](#smokestack-factory) * [factory](#smokestack-factory)
* after_build
* attribute * attribute
* before_build
@ -102,7 +152,9 @@ factory resource, variant \\ :default
Define factories for a resource Define factories for a resource
### Nested DSLs ### Nested DSLs
* [after_build](#smokestack-factory-after_build)
* [attribute](#smokestack-factory-attribute) * [attribute](#smokestack-factory-attribute)
* [before_build](#smokestack-factory-before_build)
@ -118,8 +170,42 @@ Define factories for a resource
| Name | Type | Default | Docs | | Name | Type | Default | Docs |
|------|------|---------|------| |------|------|---------|------|
| [`domain`](#smokestack-factory-domain){: #smokestack-factory-domain } | `module` | | The Ash Domain to use when evaluating loads | | [`domain`](#smokestack-factory-domain){: #smokestack-factory-domain } | `module` | | The Ash Domain to use when evaluating loads |
| [`auto_build`](#smokestack-factory-auto_build){: #smokestack-factory-auto_build } | `atom \| list(atom)` | `[]` | A list of relationships that should always be built when building this factory |
| [`auto_load`](#smokestack-factory-auto_load){: #smokestack-factory-auto_load } | `atom \| keyword \| list(atom \| keyword)` | `[]` | An Ash "load statement" to always apply when building this factory |
## smokestack.factory.after_build
```elixir
after_build hook
```
Modify the record after building.
Allows you to provide a function which can modify the built record before returning.
These hooks are only applied when building records and not parameters.
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`hook`](#smokestack-factory-after_build-hook){: #smokestack-factory-after_build-hook .spark-required} | `(any -> any) \| mfa` | | A function which returns an updated record |
### Introspection
Target: `Smokestack.Dsl.AfterBuild`
## smokestack.factory.attribute ## smokestack.factory.attribute
```elixir ```elixir
attribute name, generator attribute name, generator
@ -148,6 +234,36 @@ attribute name, generator
Target: `Smokestack.Dsl.Attribute` Target: `Smokestack.Dsl.Attribute`
## smokestack.factory.before_build
```elixir
before_build hook
```
Modify the attributes before building.
Allows you to provide a function which can modify the the attributes before building.
### Arguments
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`hook`](#smokestack-factory-before_build-hook){: #smokestack-factory-before_build-hook .spark-required} | `(any -> any) \| mfa` | | A function which returns an updated record |
### Introspection
Target: `Smokestack.Dsl.BeforeBuild`

View file

@ -2,9 +2,9 @@ defmodule Smokestack do
alias Spark.{Dsl, Dsl.Extension} alias Spark.{Dsl, Dsl.Extension}
@moduledoc """ @moduledoc """
Smokestack provides a way to define test factories for your Smokestack provides a way to define test factories for your [Ash
[Ash Resources](https://ash-hq.org/docs/module/ash/latest/ash-resource) Resources](https://ash-hq.org/docs/module/ash/latest/ash-resource) using a
using a convenient DSL: convenient DSL:
``` ```
defmodule MyApp.Factory do defmodule MyApp.Factory do
@ -36,8 +36,8 @@ defmodule Smokestack do
## Variants ## Variants
Sometimes you need to make slightly different factories to build a resource Sometimes you need to make slightly different factories to build a resource in
in a specific state for your test scenario. a specific state for your test scenario.
Here's an example defining an alternate `:trek` variant for the character Here's an example defining an alternate `:trek` variant for the character
factory defined above: factory defined above:
@ -58,8 +58,8 @@ defmodule Smokestack do
### Options ### Options
- `load`: an atom, list of atoms or keyword list of the same listing - `load`: an atom, list of atoms or keyword list of the same listing
relationships, calculations and aggregates that should be loaded relationships, calculations and aggregates that should be loaded after the
after the record is created. record is created.
- `count`: rather than inserting just a single record, you can specify a - `count`: rather than inserting just a single record, you can specify a
number of records to be inserted. A list of records will be returned. number of records to be inserted. A list of records will be returned.
- `build`: an atom, list of atoms or keyword list of the same describing - `build`: an atom, list of atoms or keyword list of the same describing
@ -68,6 +68,8 @@ defmodule Smokestack do
used, and if not the `:default` variant will be. used, and if not the `:default` variant will be.
- `attrs`: A map or keyword list of attributes you would like to set directly - `attrs`: A map or keyword list of attributes you would like to set directly
on the created record, rather than using the value provided by the factory. on the created record, rather than using the value provided by the factory.
- `relate`: A keyword list of relationships to records (or lists of records)
to which you wish to directly relate the created record.
## Building parameters ## Building parameters
@ -76,20 +78,20 @@ defmodule Smokestack do
### Options ### Options
- `encode`: rather than returning a map or maps, provide an encoder module - `encode`: rather than returning a map or maps, provide an encoder module to
to serialise the parameters. Commonly you would use `Jason` or `Poison`. serialise the parameters. Commonly you would use `Jason` or `Poison`.
- `nest`: rather than returning a map or maps directly, wrap the result in - `nest`: rather than returning a map or maps directly, wrap the result in an
an outer map using the provided key. outer map using the provided key.
- `key_case`: change the case of the keys into one of the many cases - `key_case`: change the case of the keys into one of the many cases supported
supported by [recase](https://hex.pm/packages/recase). by [recase](https://hex.pm/packages/recase).
- `key_type`: specify whether the returned map or maps should use string or - `key_type`: specify whether the returned map or maps should use string or
atom keys (ignored when using the `encode` option). atom keys (ignored when using the `encode` option).
- `count`: rather than returning just a single map, you can specify a - `count`: rather than returning just a single map, you can specify a number
number of results to be returned. A list of maps will be returned. of results to be returned. A list of maps will be returned.
- `build`: an atom, list of atoms or keyword list of the same describing - `build`: an atom, list of atoms or keyword list of the same describing
relationships which you would like built within the result. If the relationships which you would like built within the result. If the related
related resource has a variant which matches the current one, it will be resource has a variant which matches the current one, it will be used, and
used, and if not the `:default` variant will be. if not the `:default` variant will be.
- `attrs`: A map or keyword list of attributes you would like to set directly - `attrs`: A map or keyword list of attributes you would like to set directly
on the result, rather than using the value provided by the factory. on the result, rather than using the value provided by the factory.
@ -110,7 +112,7 @@ defmodule Smokestack do
use Dsl, default_extensions: [extensions: [Smokestack.Dsl]] use Dsl, default_extensions: [extensions: [Smokestack.Dsl]]
alias Ash.Resource alias Ash.Resource
alias Smokestack.{Builder, Dsl.Info, ParamBuilder, RecordBuilder} alias Smokestack.{Builder, ParamBuilder, RecordBuilder}
@type t :: module @type t :: module
@ -194,11 +196,7 @@ defmodule Smokestack do
@spec params(Resource.t(), [Smokestack.param_option()]) :: @spec params(Resource.t(), [Smokestack.param_option()]) ::
{:ok, ParamBuilder.result()} | {:error, any} {:ok, ParamBuilder.result()} | {:error, any}
def params(resource, options \\ []) do def params(resource, options \\ []) do
{variant, options} = Keyword.pop(options, :variant, :default) Builder.build(__MODULE__, resource, ParamBuilder, options)
with {:ok, factory} <- Info.factory(__MODULE__, resource, variant) do
Builder.build(ParamBuilder, factory, options)
end
end end
@doc """ @doc """
@ -227,11 +225,7 @@ defmodule Smokestack do
@spec insert(Resource.t(), [Smokestack.insert_option()]) :: @spec insert(Resource.t(), [Smokestack.insert_option()]) ::
{:ok, RecordBuilder.result()} | {:error, any} {:ok, RecordBuilder.result()} | {:error, any}
def insert(resource, options \\ []) do def insert(resource, options \\ []) do
{variant, options} = Keyword.pop(options, :variant, :default) Builder.build(__MODULE__, resource, RecordBuilder, options)
with {:ok, factory} <- Info.factory(__MODULE__, resource, variant) do
Builder.build(RecordBuilder, factory, options)
end
end end
@doc """ @doc """

View file

@ -3,7 +3,8 @@ defmodule Smokestack.Builder do
A generic behaviour for "building things". A generic behaviour for "building things".
""" """
alias Smokestack.Dsl.Factory alias Ash.Resource
alias Smokestack.Dsl.{Factory, Info}
alias Spark.Options alias Spark.Options
@type result :: any @type result :: any
@ -18,14 +19,18 @@ defmodule Smokestack.Builder do
@doc """ @doc """
Provide a schema for validating options. Provide a schema for validating options.
""" """
@callback option_schema(nil | Factory.t()) :: {:ok, Options.schema()} | {:error, any} @callback option_schema(nil | Factory.t()) ::
{:ok, Options.schema(), String.t()} | {:error, any}
@doc """ @doc """
Given a builder and a factory, validate it's options and call the builder. Find the appropriate factory, validate options and run the builder.
""" """
@spec build(t, Factory.t(), Keyword.t()) :: {:ok, result} | {:error, error} @spec build(Smokestack.t(), Resource.t(), t, Keyword.t()) :: {:ok, result} | {:error, error}
def build(builder, factory, options) do def build(factory_module, resource, builder, options) do
with {:ok, schema} <- builder.option_schema(factory), with {:ok, our_schema} <- variant_schema(factory_module, resource),
{:ok, factory} <- Info.factory(factory_module, resource, options[:variant] || :default),
{:ok, builder_schema, section} <- builder.option_schema(factory),
schema <- Options.merge(our_schema, builder_schema, section),
{:ok, options} <- Options.validate(options, schema) do {:ok, options} <- Options.validate(options, schema) do
builder.build(factory, options) builder.build(factory, options)
end end
@ -36,7 +41,29 @@ defmodule Smokestack.Builder do
""" """
@spec docs(t, nil | Factory.t()) :: String.t() @spec docs(t, nil | Factory.t()) :: String.t()
def docs(builder, factory) do def docs(builder, factory) do
{:ok, schema} = builder.option_schema(factory) {:ok, schema, _} = builder.option_schema(factory)
Options.docs(schema) Options.docs(schema)
end end
defp variant_schema(factory_module, resource) do
case Info.variants(factory_module, resource) do
[] ->
{:error,
"There are no factories defined for the resource `#{inspect(resource)}` in the `#{inspect(factory_module)}` module."}
variants ->
our_schema = [
variant: [
type: {:in, variants},
required: false,
default: :default,
doc: """
The name of the factory variant to use.
"""
]
]
{:ok, our_schema}
end
end
end end

View file

@ -4,7 +4,7 @@ defmodule Smokestack.FactoryBuilder do
""" """
alias Ash.Resource alias Ash.Resource
alias Smokestack.{Builder, Dsl.Attribute, Dsl.Factory, Template} alias Smokestack.{Builder, Dsl.Factory, Template}
alias Spark.Options alias Spark.Options
@behaviour Builder @behaviour Builder
@ -24,25 +24,31 @@ defmodule Smokestack.FactoryBuilder do
@impl true @impl true
@spec build(Factory.t(), [option]) :: {:ok, result} | {:error, error} @spec build(Factory.t(), [option]) :: {:ok, result} | {:error, error}
def build(factory, options) do def build(factory, options) do
overrides = options[:attrs] overrides = Keyword.get(options, :attrs, %{})
factory with {:ok, overrides} <- validate_overrides(factory, overrides) do
|> Map.get(:attributes, []) attrs =
|> Enum.filter(&is_struct(&1, Attribute)) factory.attributes
|> Enum.reduce({:ok, %{}}, fn |> remove_overridden_attrs(overrides)
attr, {:ok, attrs} when is_map_key(overrides, attr.name) -> |> Enum.reduce(overrides, fn attr, attrs ->
{:ok, Map.put(attrs, attr.name, Map.get(overrides, attr.name))} generator = maybe_initialise_generator(attr)
value = Template.generate(generator, attrs, options)
Map.put(attrs, attr.name, value)
end)
attr, {:ok, attrs} -> attrs =
generator = maybe_initialise_generator(attr) factory.before_build
value = Template.generate(generator, attrs, options) |> Enum.reduce(attrs, fn hook, attrs ->
{:ok, Map.put(attrs, attr.name, value)} hook.hook.(attrs)
end) end)
{:ok, attrs}
end
end end
@doc false @doc false
@impl true @impl true
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema()} | {:error, error} @spec option_schema(nil | Factory.t()) :: {:ok, Options.schema(), String.t()} | {:error, error}
def option_schema(factory) do def option_schema(factory) do
attr_keys = attr_keys =
if factory do if factory do
@ -74,7 +80,7 @@ defmodule Smokestack.FactoryBuilder do
``` ```
""" """
] ]
]} ], "Options for building instances"}
end end
defp maybe_initialise_generator(attr) do defp maybe_initialise_generator(attr) do
@ -84,4 +90,26 @@ defmodule Smokestack.FactoryBuilder do
generator generator
end end
end end
defp validate_overrides(factory, overrides) do
valid_attr_names =
factory.resource
|> Resource.Info.attributes()
|> Enum.map(& &1.name)
Enum.reduce_while(overrides, {:ok, overrides}, fn {key, _}, {:ok, overrides} ->
if key in valid_attr_names do
{:cont, {:ok, overrides}}
else
{:halt,
{:error,
"No attribute named `#{inspect(key)}` available on resource `#{inspect(factory.resource)}`"}}
end
end)
end
defp remove_overridden_attrs(attrs, overrides) when map_size(overrides) == 0, do: attrs
defp remove_overridden_attrs(attrs, overrides),
do: Enum.reject(attrs, &is_map_key(overrides, &1.name))
end end

View file

@ -30,9 +30,9 @@ defmodule Smokestack.ManyBuilder do
@doc false @doc false
@impl true @impl true
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema()} | {:error, error} @spec option_schema(nil | Factory.t()) :: {:ok, Options.schema(), String.t()} | {:error, error}
def option_schema(factory) do def option_schema(factory) do
with {:ok, related_schema} <- RelatedBuilder.option_schema(factory) do with {:ok, related_schema, related_section} <- RelatedBuilder.option_schema(factory) do
schema = schema =
[ [
count: [ count: [
@ -53,16 +53,16 @@ defmodule Smokestack.ManyBuilder do
""" """
] ]
] ]
|> Options.merge(related_schema, "Options for building relationships") |> Options.merge(related_schema, related_section)
{:ok, schema} {:ok, schema, "Options for building multiple instances"}
end end
end end
defp do_build(factory, how_many, options) when how_many > 0 and is_integer(how_many) do defp do_build(factory, how_many, options) when how_many > 0 and is_integer(how_many) do
1..how_many 1..how_many
|> Enum.reduce_while({:ok, []}, fn _, {:ok, results} -> |> Enum.reduce_while({:ok, []}, fn _, {:ok, results} ->
case Builder.build(RelatedBuilder, factory, options) do case RelatedBuilder.build(factory, options) do
{:ok, attrs} -> {:cont, {:ok, [attrs | results]}} {:ok, attrs} -> {:cont, {:ok, [attrs | results]}}
{:error, reason} -> {:halt, {:error, reason}} {:error, reason} -> {:halt, {:error, reason}}
end end

View file

@ -69,10 +69,10 @@ defmodule Smokestack.ParamBuilder do
@doc false @doc false
@impl true @impl true
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema()} | {:error, error} @spec option_schema(nil | Factory.t()) :: {:ok, Options.schema(), String.t()} | {:error, error}
def option_schema(factory) do def option_schema(factory) do
with {:ok, related_schema} <- RelatedBuilder.option_schema(factory), with {:ok, related_schema, related_section} <- RelatedBuilder.option_schema(factory),
{:ok, many_schema} <- ManyBuilder.option_schema(factory) do {:ok, many_schema, many_section} <- ManyBuilder.option_schema(factory) do
many_schema = many_schema =
Keyword.update!(many_schema, :count, fn current -> Keyword.update!(many_schema, :count, fn current ->
current current
@ -178,10 +178,10 @@ defmodule Smokestack.ParamBuilder do
schema = schema =
our_schema our_schema
|> Options.merge(many_schema, "Options for building multiple instances") |> Options.merge(many_schema, many_section)
|> Options.merge(related_schema, "Options for building relationships") |> Options.merge(related_schema, related_section)
{:ok, schema} {:ok, schema, "Options for building parameters"}
end end
end end
@ -189,7 +189,7 @@ defmodule Smokestack.ParamBuilder do
{my_opts, their_opts} = split_options(options) {my_opts, their_opts} = split_options(options)
their_opts = Keyword.put(their_opts, :count, count) their_opts = Keyword.put(their_opts, :count, count)
with {:ok, attr_list} <- Builder.build(ManyBuilder, factory, their_opts) do with {:ok, attr_list} <- ManyBuilder.build(factory, their_opts) do
attr_list attr_list
|> convert_keys(my_opts) |> convert_keys(my_opts)
|> maybe_nest_result(my_opts[:nest]) |> maybe_nest_result(my_opts[:nest])
@ -200,7 +200,7 @@ defmodule Smokestack.ParamBuilder do
defp do_build(factory, options, _) do defp do_build(factory, options, _) do
{my_opts, their_opts} = split_options(options) {my_opts, their_opts} = split_options(options)
with {:ok, attrs} <- Builder.build(RelatedBuilder, factory, their_opts) do with {:ok, attrs} <- RelatedBuilder.build(factory, their_opts) do
attrs attrs
|> convert_keys(my_opts) |> convert_keys(my_opts)
|> maybe_nest_result(my_opts[:nest]) |> maybe_nest_result(my_opts[:nest])

View file

@ -31,10 +31,10 @@ defmodule Smokestack.RecordBuilder do
@doc false @doc false
@impl true @impl true
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema()} | {:error, error} @spec option_schema(nil | Factory.t()) :: {:ok, Options.schema(), String.t()} | {:error, error}
def option_schema(factory) do def option_schema(factory) do
with {:ok, related_schema} <- RelatedBuilder.option_schema(factory), with {:ok, related_schema, related_section} <- RelatedBuilder.option_schema(factory),
{:ok, many_schema} <- ManyBuilder.option_schema(factory) do {:ok, many_schema, many_section} <- ManyBuilder.option_schema(factory) do
load_type = load_type =
if factory do if factory do
loadable_names = loadable_names =
@ -87,10 +87,10 @@ defmodule Smokestack.RecordBuilder do
""" """
] ]
] ]
|> Options.merge(many_schema, "Options for building multiple instances") |> Options.merge(many_schema, many_section)
|> Options.merge(related_schema, "Options for building relationships") |> Options.merge(related_schema, related_section)
{:ok, schema} {:ok, schema, "Options for building records"}
end end
end end
@ -98,18 +98,22 @@ defmodule Smokestack.RecordBuilder do
{load, options} = Keyword.pop(options, :load, []) {load, options} = Keyword.pop(options, :load, [])
options = Keyword.put(options, :count, count) options = Keyword.put(options, :count, count)
with {:ok, attr_list} <- Builder.build(ManyBuilder, factory, options), with {:ok, attr_list} <- ManyBuilder.build(factory, options),
{:ok, record_list} <- seed(attr_list, factory) do {:ok, record_list} <- seed(attr_list, factory) do
maybe_load(record_list, factory, load) record_list
|> maybe_hook(factory)
|> maybe_load(factory, List.wrap(load))
end end
end end
defp do_build(factory, options, _count) do defp do_build(factory, options, _count) do
{load, options} = Keyword.pop(options, :load, []) {load, options} = Keyword.pop(options, :load, [])
with {:ok, attrs} <- Builder.build(RelatedBuilder, factory, options), with {:ok, attrs} <- RelatedBuilder.build(factory, options),
{:ok, record} <- seed(attrs, factory) do {:ok, record} <- seed(attrs, factory) do
maybe_load(record, factory, load) record
|> maybe_hook(factory)
|> maybe_load(factory, List.wrap(load))
end end
end end
@ -141,11 +145,30 @@ defmodule Smokestack.RecordBuilder do
|> Resource.put_metadata(:variant, factory.variant) |> Resource.put_metadata(:variant, factory.variant)
end end
defp maybe_load(record_or_records, _factory, []), do: {:ok, record_or_records} defp maybe_load(record_or_records, %{auto_load: []}, []), do: {:ok, record_or_records}
defp maybe_load(_record_or_records, factory, _load) when is_nil(factory.domain), defp maybe_load(_record_or_records, factory, _load) when is_nil(factory.domain),
do: {:error, "Unable to perform `load` operation without an Domain."} do: {:error, "Unable to perform `load` operation without an Domain."}
defp maybe_load(record_or_records, factory, load), defp maybe_load(record_or_records, factory, load) do
do: Ash.load(record_or_records, load, domain: factory.domain) load =
factory.auto_load
|> Enum.concat(load)
Ash.load(record_or_records, load, domain: factory.domain)
end
defp maybe_hook(records, factory) when is_list(records) do
Enum.map(records, fn record ->
Enum.reduce(factory.after_build, record, fn hook, record ->
hook.(record)
end)
end)
end
defp maybe_hook(record, factory) when is_map(record) do
Enum.reduce(factory.after_build, record, fn hook, record ->
hook.hook.(record)
end)
end
end end

View file

@ -9,13 +9,18 @@ defmodule Smokestack.RelatedBuilder do
alias Spark.Options alias Spark.Options
@behaviour Builder @behaviour Builder
@type option :: build_option | FactoryBuilder.option() @type option :: build_option | relate_option | FactoryBuilder.option()
@typedoc """ @typedoc """
A nested keyword list of associations that should also be built. A nested keyword list of associations that should also be built.
""" """
@type build_option :: {:build, Smokestack.recursive_atom_list()} @type build_option :: {:build, Smokestack.recursive_atom_list()}
@typedoc """
A nested keyword list of previously built records to explicitly associate to the new record.
"""
@type relate_option :: {:relate, [{atom, Resource.record()}]}
@type result :: %{optional(atom) => any} @type result :: %{optional(atom) => any}
@type error :: FactoryBuilder.error() | Exception.t() @type error :: FactoryBuilder.error() | Exception.t()
@ -25,16 +30,17 @@ defmodule Smokestack.RelatedBuilder do
@impl true @impl true
@spec build(Factory.t(), [option]) :: {:ok, result} | {:error, error} @spec build(Factory.t(), [option]) :: {:ok, result} | {:error, error}
def build(factory, options) do def build(factory, options) do
with {:ok, attrs} <- Builder.build(FactoryBuilder, factory, Keyword.delete(options, :build)) do with {:ok, attrs} <- FactoryBuilder.build(factory, Keyword.drop(options, [:build, :relate])),
maybe_build_related(factory, attrs, options) {:ok, attrs} <- maybe_build_related(factory, attrs, options) do
maybe_relate(factory, attrs, options)
end end
end end
@doc false @doc false
@impl true @impl true
@spec option_schema(nil | Factory.t()) :: {:ok, Options.schema()} | {:error, error} @spec option_schema(nil | Factory.t()) :: {:ok, Options.schema(), String.t()} | {:error, error}
def option_schema(factory) do def option_schema(factory) do
with {:ok, factory_schema} <- FactoryBuilder.option_schema(factory) do with {:ok, factory_schema, factory_section} <- FactoryBuilder.option_schema(factory) do
build_type = build_type =
if factory do if factory do
relationship_names = relationship_names =
@ -55,6 +61,26 @@ defmodule Smokestack.RelatedBuilder do
{:or, [{:wrap_list, :atom}, :keyword_list]} {:or, [{:wrap_list, :atom}, :keyword_list]}
end end
relate_type =
if factory do
factory.resource
|> Resource.Info.relationships()
|> Enum.map(fn
relationship when relationship.cardinality == :one ->
{relationship.name, [type: {:struct, relationship.destination}, required: false]}
relationship when relationship.cardinality == :many ->
{relationship.name,
[type: {:wrap_list, {:struct, relationship.destination}}, required: false]}
end)
|> case do
[] -> :keyword_list
keys -> {:or, [{:keyword_list, keys}]}
end
else
:keyword_list
end
schema = schema =
[ [
build: [ build: [
@ -83,21 +109,35 @@ defmodule Smokestack.RelatedBuilder do
build one instance. build one instance.
If these caveats are an issue, then you can build them yourself and If these caveats are an issue, then you can build them yourself and
pass them in using the `attrs` option. pass them in using the `relate` option.
For example: For example:
```elixir ```elixir
posts = insert!(Post, count: 3) posts = insert!(Post, count: 3)
author = insert(Author, posts: posts) author = insert(Author, relate: [posts: posts])
``` ```
"""
],
relate: [
type: relate_type,
required: false,
default: [],
doc: """
A list of records to relate.
For example
```elixir
author = insert!(Author)
post = insert!(Post, relate: [author: author])
```
""" """
] ]
] ]
|> Options.merge(factory_schema, "Options for building instances") |> Options.merge(factory_schema, factory_section)
{:ok, schema} {:ok, schema, "Options for building relationships"}
end end
end end
@ -105,10 +145,12 @@ defmodule Smokestack.RelatedBuilder do
options options
|> Keyword.get(:build, []) |> Keyword.get(:build, [])
|> List.wrap() |> List.wrap()
|> Enum.concat(factory.auto_build)
|> Enum.map(fn |> Enum.map(fn
{key, value} -> {key, value} {key, value} -> {key, value}
key when is_atom(key) -> {key, []} key when is_atom(key) -> {key, []}
end) end)
|> remove_explicit_relates(options)
|> Enum.reduce_while({:ok, attrs}, fn {relationship, nested_builds}, {:ok, attrs} -> |> Enum.reduce_while({:ok, attrs}, fn {relationship, nested_builds}, {:ok, attrs} ->
case build_related( case build_related(
attrs, attrs,
@ -122,6 +164,18 @@ defmodule Smokestack.RelatedBuilder do
end) end)
end end
defp remove_explicit_relates(builds, options) do
relates =
options[:relate]
|> List.wrap()
|> Map.new()
builds
|> Enum.reject(fn {key, _value} ->
is_map_key(relates, key)
end)
end
defp build_related(attrs, relationship, factory, options) do defp build_related(attrs, relationship, factory, options) do
ash_relationship = Resource.Info.relationship(factory.resource, relationship) ash_relationship = Resource.Info.relationship(factory.resource, relationship)
build_related(attrs, relationship, factory, options, ash_relationship) build_related(attrs, relationship, factory, options, ash_relationship)
@ -141,8 +195,7 @@ defmodule Smokestack.RelatedBuilder do
|> Keyword.put(:attrs, %{}) |> Keyword.put(:attrs, %{})
with {:ok, related_factory} <- find_related_factory(relationship.destination, factory), with {:ok, related_factory} <- find_related_factory(relationship.destination, factory),
{:ok, related_attrs} <- {:ok, related_attrs} <- __MODULE__.build(related_factory, related_options) do
Builder.build(__MODULE__, related_factory, related_options) do
case relationship.cardinality do case relationship.cardinality do
:one -> :one ->
{:ok, Map.put(attrs, relationship.name, related_attrs)} {:ok, Map.put(attrs, relationship.name, related_attrs)}
@ -153,6 +206,53 @@ defmodule Smokestack.RelatedBuilder do
end end
end end
defp maybe_relate(factory, attrs, options) do
options
|> Keyword.get(:relate, [])
|> List.wrap()
|> Enum.map(fn {relationship, record} ->
{relationship, record, Resource.Info.relationship(factory.resource, relationship)}
end)
|> Enum.reduce_while({:ok, attrs}, fn
{relationship_name, _record, nil}, {:ok, _attrs} ->
{:halt,
{:error,
"No relationship named `#{inspect(relationship_name)}` defined on resource `#{inspect(factory.resource)}`"}}
{_, record, relationship}, {:ok, attrs} ->
case relate(attrs, relationship, record) do
{:ok, attrs} -> {:cont, {:ok, attrs}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
end
defp relate(attrs, relationship, record)
when is_struct(record, relationship.destination) and relationship.cardinality == :one,
do: {:ok, Map.put(attrs, relationship.name, record)}
defp relate(_attrs, relationship, record) when relationship.cardinality == :one,
do:
{:error,
"Expected value to be a `#{inspect(relationship.destination)}` record, however it is #{inspect(record)}"}
defp relate(attrs, relationship, records) when relationship.cardinality == :many do
records
|> Enum.reduce_while({:ok, []}, fn
record, {:ok, records} when is_struct(record, relationship.destination) ->
{:cont, {:ok, [record | records]}}
record, _ ->
{:halt,
{:error,
"Expected value to be a `#{inspect(relationship.destination)}` record, however it is #{inspect(record)}"}}
end)
|> case do
{:ok, records} -> {:ok, Map.put(attrs, relationship.name, records)}
{:error, reason} -> {:error, reason}
end
end
defp find_related_factory(resource, factory) when factory.variant == :default, defp find_related_factory(resource, factory) when factory.variant == :default,
do: Info.factory(factory.module, resource, :default) do: Info.factory(factory.module, resource, :default)

View file

@ -0,0 +1,43 @@
defmodule Smokestack.Dsl.AfterBuild do
@moduledoc """
The `after_build` DSL entity.
See `d:Smokestack.factory.after_build` for more information.
"""
defstruct __identifier__: nil, hook: nil
alias Ash.Resource
alias Spark.Dsl.Entity
@type t :: %__MODULE__{
__identifier__: any,
hook: mfa | (Resource.record() -> Resource.record())
}
@doc false
@spec __entities__ :: [Entity.t()]
def __entities__,
do: [
%Entity{
name: :after_build,
describe: """
Modify the record after building.
Allows you to provide a function which can modify the built record before returning.
These hooks are only applied when building records and not parameters.
""",
target: __MODULE__,
args: [:hook],
identifier: {:auto, :unique_integer},
schema: [
hook: [
type: {:mfa_or_fun, 1},
required: true,
doc: "A function which returns an updated record"
]
]
}
]
end

View file

@ -2,7 +2,7 @@ defmodule Smokestack.Dsl.Attribute do
@moduledoc """ @moduledoc """
The `attribute ` DSL entity. The `attribute ` DSL entity.
See `d:Smokestack.factory.default.attribute` for more information. See `d:Smokestack.factory.attribute` for more information.
""" """
defstruct __identifier__: nil, generator: nil, name: nil defstruct __identifier__: nil, generator: nil, name: nil
@ -11,7 +11,7 @@ defmodule Smokestack.Dsl.Attribute do
alias Spark.Dsl.Entity alias Spark.Dsl.Entity
@type t :: %__MODULE__{ @type t :: %__MODULE__{
__identifier__: nil, __identifier__: any,
generator: generator:
mfa | (-> any) | (Resource.record() -> any) | (Resource.record(), keyword -> any), mfa | (-> any) | (Resource.record() -> any) | (Resource.record(), keyword -> any),
name: atom name: atom

View file

@ -0,0 +1,41 @@
defmodule Smokestack.Dsl.BeforeBuild do
@moduledoc """
The `before_build` DSL entity.
See `d:Smokestack.factory.before_build` for more information.
"""
defstruct __identifier__: nil, hook: nil
alias Spark.Dsl.Entity
@type attrs :: %{required(String.t() | atom) => any}
@type t :: %__MODULE__{
__identifier__: any,
hook: mfa | (attrs -> attrs)
}
@doc false
@spec __entities__ :: [Entity.t()]
def __entities__,
do: [
%Entity{
name: :before_build,
describe: """
Modify the attributes before building.
Allows you to provide a function which can modify the the attributes before building.
""",
target: __MODULE__,
args: [:hook],
identifier: {:auto, :unique_integer},
schema: [
hook: [
type: {:mfa_or_fun, 1},
required: true,
doc: "A function which returns an updated record"
]
]
}
]
end

View file

@ -6,19 +6,27 @@ defmodule Smokestack.Dsl.Factory do
""" """
defstruct __identifier__: nil, defstruct __identifier__: nil,
after_build: [],
attributes: [], attributes: [],
auto_load: [],
auto_build: [],
before_build: [],
domain: nil, domain: nil,
module: nil, module: nil,
resource: nil, resource: nil,
variant: :default variant: :default
alias Ash.Resource alias Ash.Resource
alias Smokestack.Dsl.{Attribute, Template} alias Smokestack.Dsl.{AfterBuild, Attribute, BeforeBuild, Template}
alias Spark.Dsl.Entity alias Spark.Dsl.Entity
@type t :: %__MODULE__{ @type t :: %__MODULE__{
__identifier__: any, __identifier__: any,
after_build: [AfterBuild.t()],
attributes: [Attribute.t()], attributes: [Attribute.t()],
auto_load: [atom] | Keyword.t(),
auto_build: [atom],
before_build: [BeforeBuild.t()],
domain: nil, domain: nil,
module: module, module: module,
resource: Resource.t(), resource: Resource.t(),
@ -52,9 +60,25 @@ defmodule Smokestack.Dsl.Factory do
required: false, required: false,
doc: "The name of a factory variant", doc: "The name of a factory variant",
default: :default default: :default
],
auto_build: [
type: {:wrap_list, :atom},
required: false,
doc: "A list of relationships that should always be built when building this factory",
default: []
],
auto_load: [
type: {:wrap_list, {:or, [:atom, :keyword_list]}},
required: false,
doc: "An Ash \"load statement\" to always apply when building this factory",
default: []
] ]
], ],
entities: [attributes: Attribute.__entities__()] entities: [
after_build: AfterBuild.__entities__(),
attributes: Attribute.__entities__(),
before_build: BeforeBuild.__entities__()
]
} }
] ]
end end

View file

@ -27,4 +27,24 @@ defmodule Smokestack.Dsl.Info do
{:ok, factory} {:ok, factory}
end end
end end
@doc "Raising version of `factory/3`"
def factory!(factory, resource, variant) do
case factory(factory, resource, variant) do
{:ok, factory} -> factory
{:error, reason} -> raise reason
end
end
@doc """
List all variants available for a resource.
"""
@spec variants(Smokestack.t(), Resource.t()) :: [atom]
def variants(factory, resource) do
factory
|> Extension.get_entities([:smokestack])
|> Enum.filter(&(is_struct(&1, Factory) && &1.resource == resource))
|> Enum.map(& &1.variant)
|> Enum.uniq()
end
end end

View file

@ -9,7 +9,11 @@ defmodule Smokestack.Dsl.Verifier do
@impl true @impl true
@spec verify(Dsl.t()) :: :ok | {:error, DslError.t()} @spec verify(Dsl.t()) :: :ok | {:error, DslError.t()}
def verify(dsl_state) do def verify(dsl_state) do
error_info = %{module: Verifier.get_persisted(dsl_state, :module), path: [:smokestack]} error_info = %{
module: Verifier.get_persisted(dsl_state, :module),
path: [:smokestack],
dsl_state: dsl_state
}
factories = factories =
dsl_state dsl_state
@ -68,7 +72,9 @@ defmodule Smokestack.Dsl.Verifier do
error_info = error_info =
Map.merge(error_info, %{resource: factory.resource, path: [:factory | error_info.path]}) Map.merge(error_info, %{resource: factory.resource, path: [:factory | error_info.path]})
with :ok <- verify_unique_attributes(factory, error_info) do with :ok <- verify_unique_attributes(factory, error_info),
:ok <- verify_auto_build(factory, error_info),
:ok <- verify_auto_load(factory, error_info) do
factory factory
|> Map.get(:attributes, []) |> Map.get(:attributes, [])
|> Enum.filter(&is_struct(&1, Attribute)) |> Enum.filter(&is_struct(&1, Attribute))
@ -186,4 +192,76 @@ defmodule Smokestack.Dsl.Verifier do
)} )}
end end
end end
defp verify_auto_build(factory, error_info) do
error_info = %{error_info | path: [:auto_build | error_info.path]}
Enum.reduce_while(factory.auto_build, :ok, fn relationship, :ok ->
error_info = %{error_info | path: [relationship | error_info.path]}
with {:ok, relationship} <- verify_relationship(factory.resource, relationship, error_info),
:ok <- verify_factory_exists(relationship.destination, error_info) do
{:cont, :ok}
else
{:error, error} -> {:halt, {:error, error}}
end
end)
end
defp verify_relationship(resource, relationship, error_info) do
case Info.relationship(resource, relationship) do
nil ->
{:error,
DslError.exception(
module: error_info.module,
path: Enum.reverse(error_info.path),
message:
"The resource `#{inspect(resource)}` has no relationship named `#{inspect(relationship)}`."
)}
relationship ->
{:ok, relationship}
end
end
defp verify_factory_exists(resource, error_info) do
factory_exists? =
error_info.dsl_state
|> Verifier.get_entities([:smokestack])
|> Enum.any?(&(is_struct(&1, Factory) && &1.resource == resource))
if factory_exists? do
:ok
else
{:error,
DslError.exception(
module: error_info.module,
path: Enum.reverse(error_info.path),
message: "No factories defined for resource `#{inspect(resource)}`."
)}
end
end
defp verify_auto_load(factory, error_info) do
error_info = %{error_info | path: [:auto_load | error_info.path]}
Enum.reduce_while(factory.auto_load, :ok, fn load, :ok ->
error_info = %{error_info | path: [load | error_info.path]}
with nil <- Info.calculation(factory.resource, load),
nil <- Info.aggregate(factory.resource, load),
nil <- Info.relationship(factory.resource, load) do
{:halt,
{:error,
DslError.exception(
module: error_info.module,
path: Enum.reverse(error_info.path),
message:
"Expected an aggregate, calculation or relationship named `#{inspect(load)}` on resource `#{inspect(factory.resource)}`"
)}}
else
_ -> {:cont, :ok}
end
end)
end
end end

View file

@ -1,7 +1,7 @@
defmodule Smokestack.MixProject do defmodule Smokestack.MixProject do
use Mix.Project use Mix.Project
@version "0.6.1-rc.2" @version "0.9.1"
@moduledoc """ @moduledoc """
Test factories for Ash resources. Test factories for Ash resources.
@ -93,7 +93,7 @@ defmodule Smokestack.MixProject do
opts = [only: ~w[dev test]a, runtime: false] opts = [only: ~w[dev test]a, runtime: false]
[ [
{:ash, "~> 3.0.0-rc.8"}, {:ash, "~> 3.0"},
{:credo, "~> 1.7", opts}, {:credo, "~> 1.7", opts},
{:dialyxir, "~> 1.3", opts}, {:dialyxir, "~> 1.3", opts},
{:doctor, "~> 0.21", opts}, {:doctor, "~> 0.21", opts},
@ -103,7 +103,7 @@ defmodule Smokestack.MixProject do
{:faker, "~> 0.18", opts}, {:faker, "~> 0.18", opts},
{:git_ops, "~> 2.6", opts}, {:git_ops, "~> 2.6", opts},
{:mix_audit, "~> 2.1", opts}, {:mix_audit, "~> 2.1", opts},
{:recase, "~> 0.7"}, {:recase, "~> 0.8"},
{:spark, "~> 2.1"} {:spark, "~> 2.1"}
] ]
end end

View file

@ -1,37 +1,41 @@
%{ %{
"ash": {:hex, :ash, "3.0.0-rc.8", "775e14c3ca7f20a73cf92fdfd6f4244712658f06a860aaff915bc471b2c82881", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.8", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.7 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.6", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f74451def7ae71f33e82fc3263312dd6521705c0db4176d1f6f02fec32b10dc0"}, "ash": {:hex, :ash, "3.4.12", "88c38273e4f400e3a696c7cac579a6684fb88aee88e7667ffd9e9cca86168b99", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.3.36 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.9", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "10fb78c879e5f0e4cc0c056d9abe8eaaba4292b8e5d096b15a1af27d59fc884a"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"}, "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
"credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
"doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"}, "doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"},
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, "earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"},
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "earmark_parser": {:hex, :earmark_parser, "1.4.40", "f3534689f6b58f48aa3a9ac850d4f05832654fe257bf0549c08cc290035f70d5", [:mix], [], "hexpm", "cdb34f35892a45325bad21735fadb88033bcb7c4c296a999bde769783f53e46a"},
"ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, "ecto": {:hex, :ecto, "3.12.3", "1a9111560731f6c3606924c81c870a68a34c819f6d4f03822f370ea31a582208", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9efd91506ae722f95e48dc49e70d0cb632ede3b7a23896252a60a14ac6d59165"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
"ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"},
"ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"}, "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
"faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
"git_ops": {:hex, :git_ops, "2.6.0", "e0791ee1cf5db03f2c61b7ebd70e2e95cba2bb9b9793011f26609f22c0900087", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "b98fca849b18aaf490f4ac7d1dd8c6c469b0cc3e6632562d366cab095e666ffe"}, "git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "glob_ex": {:hex, :glob_ex, "0.1.8", "f7ef872877ca2ae7a792ab1f9ff73d9c16bf46ecb028603a8a3c5283016adc07", [:mix], [], "hexpm", "9e39d01729419a60a937c9260a43981440c43aa4cadd1fa6672fecd58241c464"},
"igniter": {:hex, :igniter, "0.3.37", "ad4ec1c0d73dedf5514ac52c5e93d5daa64bf4037a17088a9a7f4d44133a5846", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "727b74a67df63cbe4c21a99707e02c50f4b7740c93cd3431fa9184a863eb064c"},
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
"mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"}, "mix_audit": {:hex, :mix_audit, "2.1.4", "0a23d5b07350cdd69001c13882a4f5fb9f90fbd4cbf2ebc190a2ee0d187ea3e9", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "fd807653cc8c1cada2911129c7eb9e985e3cc76ebf26f4dd628bb25bbcaa7099"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"reactor": {:hex, :reactor, "0.8.1", "1aec71d16083901277727c8162f6dd0f07e80f5ca98911b6ef4f2c95e6e62758", [:mix], [{:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ae3936d97a3e4a316744f70c77b85345b08b70da334024c26e6b5eb8ede1246b"}, "owl": {:hex, :owl, "0.11.0", "2cd46185d330aa2400f1c8c3cddf8d2ff6320baeff23321d1810e58127082cae", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "73f5783f0e963cc04a061be717a0dbb3e49ae0c4bfd55fb4b78ece8d33a65efe"},
"recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"}, "reactor": {:hex, :reactor, "0.10.0", "1206113c21ba69b889e072b2c189c05a7aced523b9c3cb8dbe2dab7062cb699a", [:mix], [{:igniter, "~> 0.2", [hex: :igniter, repo: "hexpm", optional: false]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4003c33e4c8b10b38897badea395e404d74d59a31beb30469a220f2b1ffe6457"},
"sourceror": {:hex, :sourceror, "1.0.2", "c5e86fdc14881f797749d1fe5df017ca66727a8146e7ee3e736605a3df78f3e6", [:mix], [], "hexpm", "832335e87d0913658f129d58b2a7dc0490ddd4487b02de6d85bca0169ec2bd79"}, "recase": {:hex, :recase, "0.8.1", "ab98cd35857a86fa5ca99036f575241d71d77d9c2ab0c39aacf1c9b61f6f7d1d", [:mix], [], "hexpm", "9fd8d63e7e43bd9ea385b12364e305778b2bbd92537e95c4b2e26fc507d5e4c2"},
"spark": {:hex, :spark, "2.1.11", "8093149dfd583b5ce2c06e1fea1faaf4125b50e4703138b2cbefb78c8f4aa07f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "1877d92ab993b860e9d828bfd72d50367c0d3a53dd84f4de5d221baf66ae8723"}, "rewrite": {:hex, :rewrite, "0.10.5", "6afadeae0b9d843b27ac6225e88e165884875e0aed333ef4ad3bf36f9c101bed", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "51cc347a4269ad3a1e7a2c4122dbac9198302b082f5615964358b4635ebf3d4f"},
"splode": {:hex, :splode, "0.2.1", "020079ec06c9e00f8b6586852e781b5e07aee6ba588f3f45dd993831c87b0511", [:mix], [], "hexpm", "d232a933666061fe1f659d9906042fa94b9b393bb1129a4fde6fa680033b2611"}, "sourceror": {:hex, :sourceror, "1.6.0", "9907884e1449a4bd7dbaabe95088ed4d9a09c3c791fb0103964e6316bc9448a7", [:mix], [], "hexpm", "e90aef8c82dacf32c89c8ef83d1416fc343cd3e5556773eeffd2c1e3f991f699"},
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"}, "spark": {:hex, :spark, "2.2.29", "a52733ff72b05a674e48d3ca7a4172fe7bec81e9116069da8b4db19030d581d9", [:mix], [{:igniter, ">= 0.3.36 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "111a0dadbb27537c7629bc03ac56fcab15056ab0b9ad985084b9adcdb48836c8"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"},
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"}, "splode": {:hex, :splode, "0.2.4", "71046334c39605095ca4bed5d008372e56454060997da14f9868534c17b84b53", [:mix], [], "hexpm", "ca3b95f0d8d4b482b5357954fec857abd0fa3ea509d623334c1328e7382044c2"},
"stream_data": {:hex, :stream_data, "1.1.1", "fd515ca95619cca83ba08b20f5e814aaf1e5ebff114659dc9731f966c9226246", [:mix], [], "hexpm", "45d0cd46bd06738463fd53f22b70042dbb58c384bb99ef4e7576e7bb7d3b8c8c"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, "yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"},
} }

View file

@ -7,14 +7,13 @@ defmodule Smokestack.FactoryBuilderTest do
test "it can build attributes from a factory" do test "it can build attributes from a factory" do
{:ok, factory} = Info.factory(Factory, Post, :default) {:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, attrs} = Builder.build(FactoryBuilder, factory, []) assert {:ok, attrs} = FactoryBuilder.build(factory, [])
assert byte_size(attrs[:title]) > 0 assert byte_size(attrs[:title]) > 0
end end
test "it allows attributes to be overridden" do test "it allows attributes to be overridden" do
{:ok, factory} = Info.factory(Factory, Post, :default) {:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, %{title: "wat"}} = assert {:ok, %{title: "wat"}} = FactoryBuilder.build(factory, attrs: %{title: "wat"})
Builder.build(FactoryBuilder, factory, attrs: %{title: "wat"})
end end
end end

View file

@ -7,14 +7,14 @@ defmodule Smokestack.ManyBuilderTest do
test "it can build a factory more than once" do test "it can build a factory more than once" do
{:ok, factory} = Info.factory(Factory, Post, :default) {:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, results} = Builder.build(ManyBuilder, factory, count: 2) assert {:ok, results} = ManyBuilder.build(factory, count: 2)
assert length(results) == 2 assert length(results) == 2
assert Enum.all?(results, &(byte_size(&1.title) > 0)) assert Enum.all?(results, &(byte_size(&1.title) > 0))
end end
test "it errors when asked to build less than one instance" do test "it errors when asked to build less than one instance" do
{:ok, factory} = Info.factory(Factory, Post, :default) {:ok, factory} = Info.factory(Factory, Post, :default)
assert {:error, reason} = Builder.build(ManyBuilder, factory, count: 0) assert {:error, reason} = ManyBuilder.build(factory, count: 0)
assert Exception.message(reason) =~ ~r/expected positive integer/i assert Exception.message(reason) =~ ~r/positive integer/i
end end
end end

View file

@ -7,35 +7,35 @@ defmodule Smokestack.RecordBuilderTest do
test "it can build a single record" do test "it can build a single record" do
{:ok, factory} = Info.factory(Factory, Post, :default) {:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, record} = Builder.build(RecordBuilder, factory, []) assert {:ok, record} = RecordBuilder.build(factory, [])
assert is_struct(record, Post) assert is_struct(record, Post)
assert record.__meta__.state == :loaded assert record.__meta__.state == :loaded
end end
test "it can build multiple records" do test "it can build multiple records" do
{:ok, factory} = Info.factory(Factory, Post, :default) {:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, records} = Builder.build(RecordBuilder, factory, count: 2) assert {:ok, records} = RecordBuilder.build(factory, count: 2)
assert length(records) == 2 assert length(records) == 2
assert Enum.all?(records, &(is_struct(&1, Post) && &1.__meta__.state == :loaded)) assert Enum.all?(records, &(is_struct(&1, Post) && &1.__meta__.state == :loaded))
end end
test "it can build directly related records" do test "it can build directly related records" do
{:ok, factory} = Info.factory(Factory, Post, :default) {:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, record} = Builder.build(RecordBuilder, factory, build: :author) assert {:ok, record} = RecordBuilder.build(factory, build: :author)
assert is_struct(record.author, Author) assert is_struct(record.author, Author)
assert record.author.__meta__.state == :loaded assert record.author.__meta__.state == :loaded
end end
test "it can build indirectly related records" do test "it can build indirectly related records" do
{:ok, factory} = Info.factory(Factory, Post, :default) {:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, record} = Builder.build(RecordBuilder, factory, build: [author: :posts]) assert {:ok, record} = RecordBuilder.build(factory, build: [author: :posts])
assert [%Post{} = post] = record.author.posts assert [%Post{} = post] = record.author.posts
assert post.__meta__.state == :loaded assert post.__meta__.state == :loaded
end end
test "it can load calculations" do test "it can load calculations" do
{:ok, factory} = Info.factory(Factory, Post, :default) {:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, record} = Builder.build(RecordBuilder, factory, load: :full_title) assert {:ok, record} = RecordBuilder.build(factory, load: :full_title)
assert record.full_title == record.title <> ": " <> record.sub_title assert record.full_title == record.title <> ": " <> record.sub_title
end end
@ -43,7 +43,7 @@ defmodule Smokestack.RecordBuilderTest do
{:ok, factory} = Info.factory(Factory, Post, :default) {:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, record} = assert {:ok, record} =
Builder.build(RecordBuilder, factory, RecordBuilder.build(factory,
load: [author: :count_of_posts], load: [author: :count_of_posts],
build: [author: :posts] build: [author: :posts]
) )
@ -55,7 +55,7 @@ defmodule Smokestack.RecordBuilderTest do
{:ok, factory} = Info.factory(Factory, Post, :default) {:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, record} = assert {:ok, record} =
Builder.build(RecordBuilder, factory, build: :author, load: [author: :posts]) RecordBuilder.build(factory, build: :author, load: [author: :posts])
assert [post] = record.author.posts assert [post] = record.author.posts
assert post.id == record.id assert post.id == record.id

View file

@ -2,19 +2,26 @@ defmodule Smokestack.RelatedBuilderTest do
@moduledoc false @moduledoc false
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Smokestack.{Builder, Dsl.Info, RelatedBuilder} alias Smokestack.{Builder, Dsl.Info, RecordBuilder, RelatedBuilder}
alias Support.{Factory, Post} alias Support.{Author, Factory, Post}
test "it can build attributes from directly related factories" do test "it can build attributes from directly related factories" do
{:ok, factory} = Info.factory(Factory, Post, :default) {:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, attrs} = Builder.build(RelatedBuilder, factory, build: :author) assert {:ok, attrs} = RelatedBuilder.build(factory, build: :author)
assert byte_size(attrs[:author][:name]) > 0 assert byte_size(attrs[:author][:name]) > 0
end end
test "it can build attributes from indirectly related factories" do test "it can build attributes from indirectly related factories" do
{:ok, factory} = Info.factory(Factory, Post, :default) {:ok, factory} = Info.factory(Factory, Post, :default)
assert {:ok, attrs} = Builder.build(RelatedBuilder, factory, build: [author: :posts]) assert {:ok, attrs} = RelatedBuilder.build(factory, build: [author: :posts])
assert [post] = attrs[:author][:posts] assert [post] = attrs[:author][:posts]
assert byte_size(post[:title]) > 0 assert byte_size(post[:title]) > 0
end end
test "it can attach directly related records" do
{:ok, factory} = Info.factory(Factory, Post, :default)
{:ok, author} = RecordBuilder.build(Info.factory!(Factory, Author, :default), [])
{:ok, attrs} = RelatedBuilder.build(factory, relate: [author: author])
assert attrs[:author] == author
end
end end

View file

@ -1,6 +1,7 @@
defmodule Smokestack.DslTest do defmodule Smokestack.DslTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Spark.Error.DslError alias Spark.Error.DslError
alias Support.Author
defmodule Post do defmodule Post do
@moduledoc false @moduledoc false
@ -18,6 +19,18 @@ defmodule Smokestack.DslTest do
attribute :title, :string attribute :title, :string
end end
relationships do
belongs_to :author, Author
end
calculations do
calculate :title_first_word, :string, expr(title |> string_split() |> at(0))
end
actions do
defaults [:read]
end
end end
defmodule Factory do defmodule Factory do
@ -107,4 +120,101 @@ defmodule Smokestack.DslTest do
assert %Post{title: title} = FactoryUser.test() assert %Post{title: title} = FactoryUser.test()
assert title =~ ~r/[a-z]+/i assert title =~ ~r/[a-z]+/i
end end
test "before build hooks can be applied" do
defmodule BeforeBuildFactory do
@moduledoc false
use Smokestack
factory Post do
attribute :title, &Faker.Company.catch_phrase/0
before_build &capitalise_title/1
end
def capitalise_title(record) do
%{record | title: String.upcase(record.title)}
end
end
title = Faker.Company.catch_phrase()
upper_title = String.upcase(title)
assert %Post{title: ^upper_title} = BeforeBuildFactory.insert!(Post, attrs: %{title: title})
end
test "after build hooks can be applied" do
defmodule AfterBuildFactory do
@moduledoc false
use Smokestack
factory Post do
attribute :title, &Faker.Company.catch_phrase/0
after_build &add_metadata/1
end
def add_metadata(record) do
Ash.Resource.put_metadata(record, :wat, true)
end
end
assert %Post{__metadata__: %{wat: true}} = AfterBuildFactory.insert!(Post)
end
test "auto builds can be specified in the factory" do
defmodule AutoBuildFactory do
@moduledoc false
use Smokestack
factory Post do
attribute :title, &Faker.Company.catch_phrase/0
auto_build :author
end
factory Author do
attribute :name, &Faker.Internet.email/0
attribute :email, &Faker.Person.name/0
end
end
assert %Post{author: %Author{}} = AutoBuildFactory.insert!(Post)
end
test "auto builds can be overridden at runtime" do
defmodule AutoBuildRelateFactory do
@moduledoc false
use Smokestack
factory Post do
attribute :title, &Faker.Company.catch_phrase/0
auto_build :author
end
factory Author do
attribute :name, &Faker.Internet.email/0
attribute :email, &Faker.Person.name/0
end
end
author = AutoBuildRelateFactory.insert!(Author)
post = AutoBuildRelateFactory.insert!(Post, relate: [author: author])
# The auto-build should not be used - the existing author should be used
assert Ash.count!(Author, domain: Support.Domain) == 1
assert post.author.id == author.id
end
test "auto loads can be specified in the factory" do
defmodule AutoLoadFactory do
@moduledoc false
use Smokestack
factory Post do
attribute :title, &Faker.Company.catch_phrase/0
auto_load :title_first_word
domain Support.Domain
end
end
assert post = AutoLoadFactory.insert!(Post)
assert post.title_first_word == post.title |> String.split(" ") |> List.first()
end
end end

View file

@ -0,0 +1,83 @@
defmodule Smokestack.OptionTest do
@moduledoc false
use ExUnit.Case, async: true
use Support.Factory
alias Support.{Author, Post}
describe "no options" do
test "a record can be generated directly from the factory" do
assert {:ok, author} = insert(Author)
assert is_binary(author.id)
assert is_binary(author.name)
assert is_binary(to_string(author.email))
end
end
describe "attrs" do
test "a record can be generated from the factory with some attributes overridden" do
assert {:ok, author} = insert(Author, attrs: %{name: "J.M. Dillard"})
assert author.name == "J.M. Dillard"
end
end
describe "count" do
test "many records can be generated from the factory" do
assert {:ok, authors} = insert(Author, count: 3)
assert length(authors) == 3
end
end
describe "build" do
test "it can build directly related records from the factory" do
assert {:ok, author} = insert(Author, build: [:posts])
assert [%Post{}] = author.posts
end
test "it can build indirectly directly related records from the factory" do
assert {:ok, post} = insert(Post, build: [author: :posts])
assert %Author{} = post.author
assert [other_post] = post.author.posts
assert other_post.id != post.id
end
end
describe "load" do
test "it can load related records at build time" do
assert {:ok, author} = insert(Author)
assert {:ok, post} = insert(Post, attrs: %{author_id: author.id}, load: [:author])
assert post.author.id == author.id
end
test "it can load calculations at build time" do
assert {:ok, post} = insert(Post, load: [:full_title])
assert post.full_title == "#{post.title}: #{post.sub_title}"
end
test "it can load aggregates at build time" do
assert {:ok, post} = insert(Post, build: [:author], load: [author: :count_of_posts])
assert post.author.count_of_posts == 1
end
end
describe "relate" do
test "it can relate records at build time" do
assert {:ok, author} = insert(Author)
assert {:ok, post} = insert(Post, relate: [author: author])
assert post.author.id == author.id
end
end
describe "variant" do
test "it can select a variant at build time" do
assert {:ok, author} = insert(Author, variant: :trek)
assert to_string(author.email) =~ ~r/\.(starfleet|rebellion)$/
end
end
test "it validates all options" do
assert {:error, error} = insert(Author, sss: 2)
message = Exception.message(error)
assert message =~ ~r/unknown option/
end
end

View file

@ -13,9 +13,9 @@ defmodule Support.Factory do
attribute :email, fn attribute :email, fn
%{name: "JL"} -> "captain@entrepreneur.starfleet" %{name: "JL"} -> "captain@entrepreneur.starfleet"
%{name: "Doc Holoday"} -> "cheifmed@voyager.starfleet" %{name: "Dr Mark"} -> "cheifmed@voyager.starfleet"
%{name: "BLT"} -> "cheifeng@voyager.starfleet" %{name: "BLT"} -> "cheifeng@voyager.starfleet"
%{name: "Cal Hudson"} -> "cal@maquis.stfu" %{name: "Cal Hudson"} -> "cal@maquis.rebellion"
end end
end end

View file

@ -38,7 +38,7 @@ defmodule Support.Post do
end end
relationships do relationships do
belongs_to :author, Support.Author belongs_to :author, Support.Author, attribute_writable?: true
end end
actions do actions do