-module(automata@cron@validator).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/automata/cron/validator.gleam").
-export([minute/1, hour/1, day_of_month/1, month/1, day_of_week/1, selector_values/4, to_string/1, validate/1]).
-export_type([selector/0, item/0, step_base/0, valid_cron/0, validation_error/0, alias_mode/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type selector() :: any | {values, list(item())}.
-type item() :: {exact, integer()} |
{range, integer(), integer()} |
{step, step_base(), integer()}.
-type step_base() :: step_any |
{step_exact, integer()} |
{step_range, integer(), integer()}.
-opaque valid_cron() :: {valid_cron,
selector(),
selector(),
selector(),
selector(),
selector()}.
-type validation_error() :: {unsupported_syntax,
automata@cron@ast:field(),
binary()} |
{invalid_number, automata@cron@ast:field(), binary()} |
{invalid_alias, automata@cron@ast:field(), binary()} |
{invalid_range, automata@cron@ast:field(), binary()} |
{invalid_step, automata@cron@ast:field(), binary()} |
{invalid_list, automata@cron@ast:field(), binary()} |
{out_of_range, automata@cron@ast:field(), integer(), integer(), integer()} |
{impossible_date, binary(), binary()}.
-type alias_mode() :: no_aliases | month_aliases | day_aliases.
-file("src/automata/cron/validator.gleam", 39).
-spec minute(valid_cron()) -> selector().
minute(Spec) ->
erlang:element(2, Spec).
-file("src/automata/cron/validator.gleam", 43).
-spec hour(valid_cron()) -> selector().
hour(Spec) ->
erlang:element(3, Spec).
-file("src/automata/cron/validator.gleam", 47).
-spec day_of_month(valid_cron()) -> selector().
day_of_month(Spec) ->
erlang:element(4, Spec).
-file("src/automata/cron/validator.gleam", 51).
-spec month(valid_cron()) -> selector().
month(Spec) ->
erlang:element(5, Spec).
-file("src/automata/cron/validator.gleam", 55).
-spec day_of_week(valid_cron()) -> selector().
day_of_week(Spec) ->
erlang:element(6, Spec).
-file("src/automata/cron/validator.gleam", 143).
-spec day_of_week_is_any(selector()) -> boolean().
day_of_week_is_any(Selector) ->
case Selector of
any ->
true;
{values, _} ->
false
end.
-file("src/automata/cron/validator.gleam", 355).
?DOC(
" Detect reserved Quartz-style extensions (`?`, `L`, `W`, `H`, `#`) that\n"
" the validator does not support. Run only after `int.parse` and\n"
" `parse_alias` have both failed, so that perfectly valid alias tokens\n"
" like `WED`, `JUL`, `THU` (which contain `W`, `L`, `H` as substrings)\n"
" still reach the alias resolver before being misclassified.\n"
).
-spec has_reserved_quartz_syntax(binary()) -> boolean().
has_reserved_quartz_syntax(Value) ->
(((gleam_stdlib:contains_string(Value, <<"?"/utf8>>) orelse gleam_stdlib:contains_string(
Value,
<<"L"/utf8>>
))
orelse gleam_stdlib:contains_string(Value, <<"W"/utf8>>))
orelse gleam_stdlib:contains_string(Value, <<"#"/utf8>>))
orelse gleam_stdlib:contains_string(Value, <<"H"/utf8>>).
-file("src/automata/cron/validator.gleam", 363).
-spec ensure_range(automata@cron@ast:field(), integer(), integer(), integer()) -> {ok,
integer()} |
{error, validation_error()}.
ensure_range(Field, Number, Min, Max) ->
case (Number < Min) orelse (Number > Max) of
true ->
{error, {out_of_range, Field, Min, Max, Number}};
false ->
{ok, Number}
end.
-file("src/automata/cron/validator.gleam", 375).
-spec parse_alias(automata@cron@ast:field(), binary(), alias_mode()) -> {ok,
integer()} |
{error, nil}.
parse_alias(_, Value, Aliases) ->
Upper = string:uppercase(Value),
case Aliases of
no_aliases ->
{error, nil};
month_aliases ->
case Upper of
<<"JAN"/utf8>> ->
{ok, 1};
<<"FEB"/utf8>> ->
{ok, 2};
<<"MAR"/utf8>> ->
{ok, 3};
<<"APR"/utf8>> ->
{ok, 4};
<<"MAY"/utf8>> ->
{ok, 5};
<<"JUN"/utf8>> ->
{ok, 6};
<<"JUL"/utf8>> ->
{ok, 7};
<<"AUG"/utf8>> ->
{ok, 8};
<<"SEP"/utf8>> ->
{ok, 9};
<<"OCT"/utf8>> ->
{ok, 10};
<<"NOV"/utf8>> ->
{ok, 11};
<<"DEC"/utf8>> ->
{ok, 12};
_ ->
{error, nil}
end;
day_aliases ->
case Upper of
<<"SUN"/utf8>> ->
{ok, 0};
<<"MON"/utf8>> ->
{ok, 1};
<<"TUE"/utf8>> ->
{ok, 2};
<<"WED"/utf8>> ->
{ok, 3};
<<"THU"/utf8>> ->
{ok, 4};
<<"FRI"/utf8>> ->
{ok, 5};
<<"SAT"/utf8>> ->
{ok, 6};
_ ->
{error, nil}
end
end.
-file("src/automata/cron/validator.gleam", 325).
-spec parse_value(
automata@cron@ast:field(),
binary(),
integer(),
integer(),
alias_mode()
) -> {ok, integer()} | {error, validation_error()}.
parse_value(Field, Value, Min, Max, Aliases) ->
case gleam_stdlib:parse_int(Value) of
{ok, Number} ->
ensure_range(Field, Number, Min, Max);
{error, _} ->
case parse_alias(Field, Value, Aliases) of
{ok, Number@1} ->
ensure_range(Field, Number@1, Min, Max);
{error, nil} ->
case has_reserved_quartz_syntax(Value) of
true ->
{error, {unsupported_syntax, Field, Value}};
false ->
case Aliases of
no_aliases ->
{error, {invalid_number, Field, Value}};
_ ->
{error, {invalid_alias, Field, Value}}
end
end
end
end.
-file("src/automata/cron/validator.gleam", 254).
-spec parse_step_base(
automata@cron@ast:field(),
binary(),
integer(),
integer(),
alias_mode()
) -> {ok, step_base()} | {error, validation_error()}.
parse_step_base(Field, Base, Min, Max, Aliases) ->
case Base of
<<"*"/utf8>> ->
{ok, step_any};
_ ->
case gleam_stdlib:contains_string(Base, <<"-"/utf8>>) of
true ->
case gleam@string:split(Base, <<"-"/utf8>>) of
[Start_text, End_text] ->
case (Start_text =:= <<""/utf8>>) orelse (End_text
=:= <<""/utf8>>) of
true ->
{error, {invalid_range, Field, Base}};
false ->
case parse_value(
Field,
Start_text,
Min,
Max,
Aliases
) of
{error, Error} ->
{error, Error};
{ok, Start} ->
case parse_value(
Field,
End_text,
Min,
Max,
Aliases
) of
{error, Error@1} ->
{error, Error@1};
{ok, End} ->
case Start =< End of
true ->
{ok,
{step_range,
Start,
End}};
false ->
{error,
{invalid_range,
Field,
Base}}
end
end
end
end;
_ ->
{error, {invalid_range, Field, Base}}
end;
false ->
case parse_value(Field, Base, Min, Max, Aliases) of
{ok, Value} ->
{ok, {step_exact, Value}};
{error, Error@2} ->
{error, Error@2}
end
end
end.
-file("src/automata/cron/validator.gleam", 225).
-spec parse_step(
automata@cron@ast:field(),
binary(),
integer(),
integer(),
alias_mode()
) -> {ok, item()} | {error, validation_error()}.
parse_step(Field, Part, Min, Max, Aliases) ->
case gleam@string:split(Part, <<"/"/utf8>>) of
[Base, Step_text] ->
case (Base =:= <<""/utf8>>) orelse (Step_text =:= <<""/utf8>>) of
true ->
{error, {invalid_step, Field, Part}};
false ->
case gleam_stdlib:parse_int(Step_text) of
{error, _} ->
{error, {invalid_step, Field, Part}};
{ok, Step} ->
case Step > 0 of
false ->
{error, {invalid_step, Field, Part}};
true ->
case parse_step_base(
Field,
Base,
Min,
Max,
Aliases
) of
{ok, Step_base} ->
{ok, {step, Step_base, Step}};
{error, Error} ->
{error, Error}
end
end
end
end;
_ ->
{error, {invalid_step, Field, Part}}
end.
-file("src/automata/cron/validator.gleam", 296).
-spec parse_range(
automata@cron@ast:field(),
binary(),
integer(),
integer(),
alias_mode()
) -> {ok, item()} | {error, validation_error()}.
parse_range(Field, Part, Min, Max, Aliases) ->
case gleam@string:split(Part, <<"-"/utf8>>) of
[Start_text, End_text] ->
case (Start_text =:= <<""/utf8>>) orelse (End_text =:= <<""/utf8>>) of
true ->
{error, {invalid_range, Field, Part}};
false ->
case parse_value(Field, Start_text, Min, Max, Aliases) of
{error, Error} ->
{error, Error};
{ok, Start} ->
case parse_value(Field, End_text, Min, Max, Aliases) of
{error, Error@1} ->
{error, Error@1};
{ok, End} ->
case Start =< End of
true ->
{ok, {range, Start, End}};
false ->
{error,
{invalid_range, Field, Part}}
end
end
end
end;
_ ->
{error, {invalid_range, Field, Part}}
end.
-file("src/automata/cron/validator.gleam", 204).
-spec parse_item(
automata@cron@ast:field(),
binary(),
integer(),
integer(),
alias_mode()
) -> {ok, item()} | {error, validation_error()}.
parse_item(Field, Part, Min, Max, Aliases) ->
case gleam_stdlib:contains_string(Part, <<"/"/utf8>>) of
true ->
parse_step(Field, Part, Min, Max, Aliases);
false ->
case gleam_stdlib:contains_string(Part, <<"-"/utf8>>) of
true ->
parse_range(Field, Part, Min, Max, Aliases);
false ->
case parse_value(Field, Part, Min, Max, Aliases) of
{ok, Value} ->
{ok, {exact, Value}};
{error, Error} ->
{error, Error}
end
end
end.
-file("src/automata/cron/validator.gleam", 186).
-spec parse_items(
list(binary()),
automata@cron@ast:field(),
integer(),
integer(),
alias_mode(),
list(item())
) -> {ok, selector()} | {error, validation_error()}.
parse_items(Parts, Field, Min, Max, Aliases, Acc) ->
case Parts of
[] ->
{ok, {values, lists:reverse(Acc)}};
[Part | Rest] ->
case parse_item(Field, Part, Min, Max, Aliases) of
{ok, Item} ->
parse_items(Rest, Field, Min, Max, Aliases, [Item | Acc]);
{error, Error} ->
{error, Error}
end
end.
-file("src/automata/cron/validator.gleam", 161).
-spec parse_selector(
automata@cron@ast:field(),
binary(),
integer(),
integer(),
alias_mode()
) -> {ok, selector()} | {error, validation_error()}.
parse_selector(Field, Value, Min, Max, Aliases) ->
case Value of
<<"*"/utf8>> ->
{ok, any};
_ ->
case gleam@list:any(
gleam@string:split(Value, <<","/utf8>>),
fun gleam@string:is_empty/1
) of
true ->
{error, {invalid_list, Field, Value}};
false ->
parse_items(
gleam@string:split(Value, <<","/utf8>>),
Field,
Min,
Max,
Aliases,
[]
)
end
end.
-file("src/automata/cron/validator.gleam", 454).
-spec stepped_values(integer(), integer(), integer()) -> list(integer()).
stepped_values(Start, Stop, Step) ->
case Start > Stop of
true ->
[];
false ->
[Start | stepped_values(Start + Step, Stop, Step)]
end.
-file("src/automata/cron/validator.gleam", 461).
-spec inclusive_range(integer(), integer()) -> list(integer()).
inclusive_range(Start, Stop) ->
case Start > Stop of
true ->
[];
false ->
[Start | inclusive_range(Start + 1, Stop)]
end.
-file("src/automata/cron/validator.gleam", 441).
-spec item_values(item(), integer(), integer()) -> list(integer()).
item_values(Item, Min, Max) ->
case Item of
{exact, Value} ->
[Value];
{range, Start, End} ->
inclusive_range(Start, End);
{step, Base, Step} ->
case Base of
step_any ->
stepped_values(Min, Max, Step);
{step_exact, Start@1} ->
stepped_values(Start@1, Max, Step);
{step_range, Start@2, End@1} ->
stepped_values(Start@2, End@1, Step)
end
end.
-file("src/automata/cron/validator.gleam", 475).
-spec dedup_sorted(list(integer()), integer(), list(integer())) -> list(integer()).
dedup_sorted(Rest, Previous, Acc) ->
case Rest of
[] ->
lists:reverse(Acc);
[Value | Tail] ->
case Value =:= Previous of
true ->
dedup_sorted(Tail, Previous, Acc);
false ->
dedup_sorted(Tail, Value, [Value | Acc])
end
end.
-file("src/automata/cron/validator.gleam", 468).
-spec dedup_sort(list(integer()), list(integer())) -> list(integer()).
dedup_sort(Values, Acc) ->
case gleam@list:sort(Values, fun gleam@int:compare/2) of
[] ->
lists:reverse(Acc);
[First | Rest] ->
dedup_sorted(Rest, First, [First | Acc])
end.
-file("src/automata/cron/validator.gleam", 414).
-spec selector_values(selector(), integer(), integer(), boolean()) -> list(integer()).
selector_values(Selector, Min, Max, Normalize_day_of_week) ->
Values = case Selector of
any ->
inclusive_range(Min, Max);
{values, Items} ->
_pipe = Items,
gleam@list:flat_map(
_pipe,
fun(Item) -> item_values(Item, Min, Max) end
)
end,
case Normalize_day_of_week of
true ->
_pipe@1 = Values,
_pipe@2 = gleam@list:map(_pipe@1, fun(Value) -> case Value =:= 7 of
true ->
0;
false ->
Value
end end),
dedup_sort(_pipe@2, []);
false ->
dedup_sort(Values, [])
end.
-file("src/automata/cron/validator.gleam", 150).
-spec schedule_possible(selector(), selector()) -> boolean().
schedule_possible(Day_of_month, Month) ->
Months = selector_values(Month, 1, 12, false),
Days = selector_values(Day_of_month, 1, 31, false),
gleam@list:any(
Months,
fun(Month@1) ->
Maximum = automata@internal@calendar:days_in_month(2024, Month@1),
gleam@list:any(Days, fun(Day) -> Day =< Maximum end) orelse gleam@list:any(
Days,
fun(Day@1) ->
automata@internal@calendar:days_in_month(2025, Month@1) >= Day@1
end
)
end
).
-file("src/automata/cron/validator.gleam", 501).
-spec step_base_to_string(step_base()) -> binary().
step_base_to_string(Base) ->
case Base of
step_any ->
<<"*"/utf8>>;
{step_exact, Value} ->
erlang:integer_to_binary(Value);
{step_range, Start, End} ->
<<<<(erlang:integer_to_binary(Start))/binary, "-"/utf8>>/binary,
(erlang:integer_to_binary(End))/binary>>
end.
-file("src/automata/cron/validator.gleam", 493).
-spec item_to_string(item()) -> binary().
item_to_string(Item) ->
case Item of
{exact, Value} ->
erlang:integer_to_binary(Value);
{range, Start, End} ->
<<<<(erlang:integer_to_binary(Start))/binary, "-"/utf8>>/binary,
(erlang:integer_to_binary(End))/binary>>;
{step, Base, Step} ->
<<<<(step_base_to_string(Base))/binary, "/"/utf8>>/binary,
(erlang:integer_to_binary(Step))/binary>>
end.
-file("src/automata/cron/validator.gleam", 486).
-spec selector_to_string(selector()) -> binary().
selector_to_string(Selector) ->
case Selector of
any ->
<<"*"/utf8>>;
{values, Items} ->
gleam@string:join(
gleam@list:map(Items, fun item_to_string/1),
<<","/utf8>>
)
end.
-file("src/automata/cron/validator.gleam", 115).
-spec to_string(valid_cron()) -> binary().
to_string(Spec) ->
gleam@string:join(
[selector_to_string(erlang:element(2, Spec)),
selector_to_string(erlang:element(3, Spec)),
selector_to_string(erlang:element(4, Spec)),
selector_to_string(erlang:element(5, Spec)),
selector_to_string(erlang:element(6, Spec))],
<<" "/utf8>>
).
-file("src/automata/cron/validator.gleam", 128).
-spec validate_semantics(valid_cron()) -> {ok, nil} |
{error, validation_error()}.
validate_semantics(Spec) ->
case day_of_week_is_any(erlang:element(6, Spec)) of
false ->
{ok, nil};
true ->
case schedule_possible(
erlang:element(4, Spec),
erlang:element(5, Spec)
) of
true ->
{ok, nil};
false ->
{error,
{impossible_date,
selector_to_string(erlang:element(4, Spec)),
selector_to_string(erlang:element(5, Spec))}}
end
end.
-file("src/automata/cron/validator.gleam", 76).
-spec validate(automata@cron@ast:raw_cron()) -> {ok, valid_cron()} |
{error, validation_error()}.
validate(Raw) ->
case parse_selector(minute, erlang:element(2, Raw), 0, 59, no_aliases) of
{error, Error} ->
{error, Error};
{ok, Minute} ->
case parse_selector(hour, erlang:element(3, Raw), 0, 23, no_aliases) of
{error, Error@1} ->
{error, Error@1};
{ok, Hour} ->
case parse_selector(
day_of_month,
erlang:element(4, Raw),
1,
31,
no_aliases
) of
{error, Error@2} ->
{error, Error@2};
{ok, Day_of_month} ->
case parse_selector(
month,
erlang:element(5, Raw),
1,
12,
month_aliases
) of
{error, Error@3} ->
{error, Error@3};
{ok, Month} ->
case parse_selector(
day_of_week,
erlang:element(6, Raw),
0,
7,
day_aliases
) of
{error, Error@4} ->
{error, Error@4};
{ok, Day_of_week} ->
Spec = {valid_cron,
Minute,
Hour,
Day_of_month,
Month,
Day_of_week},
case validate_semantics(Spec) of
{ok, _} ->
{ok, Spec};
{error, Error@5} ->
{error, Error@5}
end
end
end
end
end
end.