Skip to content

Commit 6dc085c

Browse files
committed
Extracted
0 parents  commit 6dc085c

File tree

16 files changed

+863
-0
lines changed

16 files changed

+863
-0
lines changed

.formatter.exs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
spark_locals_without_parens = [random: 1]
2+
3+
[
4+
import_deps: [:spark, :reactor, :ash],
5+
inputs: [
6+
"{mix,.formatter}.exs",
7+
"{config,lib,test}/**/*.{ex,exs}"
8+
],
9+
plugins: [Spark.Formatter],
10+
locals_without_parens: spark_locals_without_parens,
11+
export: [
12+
locals_without_parens: spark_locals_without_parens
13+
]
14+
]

.github/workflows/elixir.yaml

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
name: Elixir CI
2+
3+
# Define workflow that runs when changes are pushed to the
4+
# `main` branch or pushed to a PR branch that targets the `main`
5+
# branch. Change the branch name if your project uses a
6+
# different name for the main branch like "master" or "production".
7+
on:
8+
push:
9+
branches: ["main"] # adapt branch for project
10+
pull_request:
11+
branches: ["main"] # adapt branch for project
12+
13+
# Sets the ENV `MIX_ENV` to `test` for running tests
14+
env:
15+
MIX_ENV: test
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
test:
22+
# Set up a Postgres DB service. By default, Phoenix applications
23+
# use Postgres. This creates a database for running tests.
24+
# Additional services can be defined here if required.
25+
services:
26+
db:
27+
image: postgres:12
28+
ports: ["5432:5432"]
29+
env:
30+
POSTGRES_PASSWORD: postgres
31+
options: >-
32+
--health-cmd pg_isready
33+
--health-interval 10s
34+
--health-timeout 5s
35+
--health-retries 5
36+
37+
runs-on: ubuntu-latest
38+
name: Test on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}}
39+
strategy:
40+
# Specify the OTP and Elixir versions to use when building
41+
# and running the workflow steps.
42+
matrix:
43+
otp: ["27.0.0.0"] # Define the OTP version [required]
44+
elixir: ["1.18.0"] # Define the elixir version [required]
45+
steps:
46+
# Step: Setup Elixir + Erlang image as the base.
47+
- name: Set up Elixir
48+
uses: erlef/setup-beam@v1
49+
with:
50+
otp-version: ${{matrix.otp}}
51+
elixir-version: ${{matrix.elixir}}
52+
53+
# Step: Check out the code.
54+
- name: Checkout code
55+
uses: actions/checkout@v3
56+
57+
# Step: Define how to cache deps. Restores existing cache if present.
58+
- name: Cache deps
59+
id: cache-deps
60+
uses: actions/cache@v3
61+
env:
62+
cache-name: cache-elixir-deps
63+
with:
64+
path: deps
65+
key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }}
66+
restore-keys: |
67+
${{ runner.os }}-mix-${{ env.cache-name }}-
68+
69+
# Step: Define how to cache the `_build` directory. After the first run,
70+
# this speeds up tests runs a lot. This includes not re-compiling our
71+
# project's downloaded deps every run.
72+
- name: Cache compiled build
73+
id: cache-build
74+
uses: actions/cache@v3
75+
env:
76+
cache-name: cache-compiled-build
77+
with:
78+
path: _build
79+
key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }}
80+
restore-keys: |
81+
${{ runner.os }}-mix-${{ env.cache-name }}-
82+
${{ runner.os }}-mix-
83+
84+
# Step: Conditionally bust the cache when job is re-run.
85+
# Sometimes, we may have issues with incremental builds that are fixed by
86+
# doing a full recompile. In order to not waste dev time on such trivial
87+
# issues (while also reaping the time savings of incremental builds for
88+
# *most* day-to-day development), force a full recompile only on builds
89+
# that are retried.
90+
- name: Clean to rule out incremental build as a source of flakiness
91+
if: github.run_attempt != '1'
92+
run: |
93+
mix deps.clean --all
94+
mix clean
95+
shell: sh
96+
97+
# Step: Download project dependencies. If unchanged, uses
98+
# the cached version.
99+
- name: Install dependencies
100+
run: mix deps.get
101+
102+
# Step: Compile the project treating any warnings as errors.
103+
# Customize this step if a different behavior is desired.
104+
- name: Compiles without warnings
105+
run: mix compile --warnings-as-errors
106+
107+
# Step: Check that the checked in code has already been formatted.
108+
# This step fails if something was found unformatted.
109+
# Customize this step as desired.
110+
- name: Check Formatting
111+
run: mix format --check-formatted
112+
113+
# Step: Execute the tests.
114+
- name: Run tests
115+
run: mix test

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# If the VM crashes, it generates a dump, let's ignore it too.
14+
erl_crash.dump
15+
16+
# Also ignore archive artifacts (built via "mix archive.build").
17+
*.ez
18+
19+
# Ignore package tarball (built via "mix hex.build").
20+
ash_random_params-*.tar
21+
22+
# Temporary files, for example, from tests.
23+
/tmp/
24+
*.code-workspace

.tool-versions

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
erlang 27.3
2+
elixir 1.18.3-otp-27

README.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# AshRandomParams
2+
3+
A library that generates random parameters for Ash resource actions. It provides a convenient way to create random test data for your Ash resources by automatically generating random values for accepts and arguments.
4+
5+
## Usage
6+
7+
Add the `random_params` DSL to your Ash resource:
8+
9+
```elixir
10+
defmodule Post do
11+
use Ash.Resource, extensions: [AshRandomParams]
12+
13+
attributes do
14+
uuid_primary_key :id
15+
attribute :author, :string, allow_nil?: false
16+
attribute :title, :string, allow_nil?: false
17+
attribute :content, :string, allow_nil?: true
18+
attribute :tag, :string, allow_nil?: false, default: "JS"
19+
end
20+
21+
random_params do
22+
random MyRandom # Optional: specify your custom random generator
23+
end
24+
end
25+
```
26+
27+
### Using Random Params
28+
29+
```elixir
30+
# Basic usage
31+
Post.random_params!(:create)
32+
=> %{author: "author-81491", title: "title-388112", content: nil, tag: "JS"}
33+
34+
# With initial params
35+
Post.random_params!(:create, %{author: "James"})
36+
=> %{author: "James", title: "title-388112", content: nil, tag: "JS"}
37+
38+
# With options
39+
Post.random_params!(:create, %{author: "James"}, %{
40+
populate: [:content],
41+
omit: [:title],
42+
include_defaults?: false
43+
})
44+
=> %{author: "James", content: "content-38128"}
45+
```
46+
47+
### Default Behavior
48+
49+
By default, it generates random values for attributes and arguments that have `allow_nil?: false` and no default value (`default == nil`). In the example above, `author` and `title` fall into this category.
50+
51+
### Belongs To Relationships
52+
53+
For attributes and arguments that match the `name` or `source_attribute` of a `belongs_to` relationship:
54+
- In `create` actions, they are generated with `nil` values
55+
- In other actions, they are not generated at all
56+
57+
This behavior exists because:
58+
- In `create` actions, omitting a value is equivalent to setting it to `nil`
59+
- In `update` actions, omitting a value preserves the existing relationship, while explicitly setting it to `nil` removes the relationship
60+
61+
### Options
62+
63+
- `populate`: Forces generation of random values for specified attributes/arguments, overriding the default behavior
64+
- `omit`: Prevents generation of random values for specified attributes/arguments, overriding the default behavior
65+
- `include_defaults?`: When set to `true`, includes default values for attributes/arguments that have either `allow_nil?: true` or a non-nil default value. Defaults to `true`. In the example above, this would add `%{content: nil, tag: "JS"}` to the generated params.
66+
67+
### Custom Random Generator
68+
69+
You can implement a custom random generator by using the `AshRandomParams.Random` behaviour:
70+
71+
```elixir
72+
defmodule MyRandom do
73+
use AshRandomParams.Random
74+
75+
@impl AshRandomParams.Random
76+
def random(%{type: Ash.Type.Integer}, _opts, _context) do
77+
777
78+
end
79+
80+
@impl AshRandomParams.Random
81+
def random(attr_or_arg, opts, context) do
82+
# Fallback to DefaultRandom for all other types
83+
AshRandomParams.DefaultRandom.random(attr_or_arg, opts, context)
84+
end
85+
end
86+
```
87+
88+
## Features
89+
90+
- Automatically generates random values for action accepts and arguments
91+
- Supports custom random value generators
92+
- Useful for testing and development
93+
94+
## Installation
95+
96+
Add `ash_random_params` to your list of dependencies in `mix.exs`:
97+
98+
```elixir
99+
def deps do
100+
[
101+
{:ash_random_params, "~> 0.1.0"}
102+
]
103+
end
104+
```
105+
106+
## License
107+
108+
MIT
109+
110+
## Links
111+
112+
- [GitHub Repository](https://github.com/devall-org/ash_random_params)

config/config.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Config
2+
3+
config :ash, :validate_domain_config_inclusion?, false

lib/ash_random_params.ex

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
defmodule AshRandomParams do
2+
@random_params %Spark.Dsl.Section{
3+
name: :random_params,
4+
describe: """
5+
random_params configuration
6+
""",
7+
schema: [
8+
random: [
9+
type:
10+
{:spark_function_behaviour, AshRandomParams.Random, {AshRandomParams.RandomFunction, 4}},
11+
required: false,
12+
default: {AshRandomParams.DefaultRandom, []},
13+
doc: """
14+
random attribute, argument generation function
15+
"""
16+
]
17+
],
18+
examples: [
19+
"""
20+
random_params do
21+
random MyRandom
22+
end
23+
"""
24+
],
25+
entities: []
26+
}
27+
28+
use Spark.Dsl.Extension,
29+
sections: [@random_params],
30+
transformers: [AshRandomParams.Transformer]
31+
end

lib/info.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
defmodule AshRandomParams.Info do
2+
use Spark.InfoGenerator, extension: AshRandomParams, sections: [:random_params]
3+
end

lib/random/default_random.ex

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
defmodule AshRandomParams.DefaultRandom do
2+
use AshRandomParams.Random
3+
4+
# Array
5+
def random(
6+
%{type: {:array, type}, constraints: constraints, name: name},
7+
_opts,
8+
%{random: {random_mod, random_opts}} = context
9+
) do
10+
min_length = constraints |> Keyword.get(:min_length, 0)
11+
12+
1..min_length//1
13+
|> Enum.map(fn _ ->
14+
random_mod.random(%{type: type, name: name}, random_opts, context)
15+
end)
16+
end
17+
18+
# One of atoms
19+
def random(%{type: Ash.Type.Atom, constraints: constraints}, _opts, _context) do
20+
constraints |> Keyword.fetch!(:one_of) |> Enum.random()
21+
end
22+
23+
def random(%{type: Ash.Type.Integer}, _opts, _context), do: random_int()
24+
25+
def random(%{type: Ash.Type.Decimal}, _opts, _context),
26+
do: Decimal.new("#{random_int()}.#{random_int()}")
27+
28+
def random(%{type: Ash.Type.Boolean}, _opts, _context), do: [true, false] |> Enum.random()
29+
def random(%{type: Ash.Type.Map}, _opts, _context), do: %{}
30+
31+
def random(%{type: Ash.Type.Date}, _opts, _context),
32+
do: random_date_time() |> DateTime.to_date()
33+
34+
def random(%{type: Ash.Type.UtcDatetime}, _opts, _context), do: random_date_time()
35+
def random(%{type: Ash.Type.UtcDatetimeUsec}, _opts, _context), do: random_date_time()
36+
def random(%{type: Ash.Type.DateTime}, _opts, _context), do: random_date_time()
37+
38+
def random(%{type: Ash.Type.String, name: name}, _opts, _context),
39+
do: "#{name}-#{random_int()}"
40+
41+
def random(%{type: Ash.Type.CiString, name: name}, _opts, _context),
42+
do: "#{name}-#{random_int()}"
43+
44+
def random(%{name: name, type: type} = attr_or_arg, opts, context) do
45+
{attr_or_arg, opts, context} |> dbg()
46+
raise "Cannot make random value for name: #{name}, type: #{type}"
47+
end
48+
49+
# Private
50+
51+
@year_seconds 365 * 24 * 60 * 60
52+
53+
defp random_date_time() do
54+
DateTime.utc_now()
55+
|> DateTime.add(:rand.uniform(20 * @year_seconds) - 10 * @year_seconds, :second)
56+
end
57+
58+
defp random_int() do
59+
:rand.uniform(1_000_000_000_000)
60+
end
61+
end

lib/random/random.ex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
defmodule AshRandomParams.Random do
2+
@callback random(
3+
attr_or_arg :: Ash.Resource.Attribute.t() | Ash.Resource.Actions.Argument.t(),
4+
opts :: keyword(),
5+
context :: map()
6+
) :: term()
7+
8+
defmacro __using__(_) do
9+
quote do
10+
@behaviour AshRandomParams.Random
11+
end
12+
end
13+
end

0 commit comments

Comments
 (0)