Skip to main content

src/fexpr.erl

-module(fexpr).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/fexpr.gleam").
-export([fexpr_value_to_json/1, fexpr_value_to_string/1, fexpr_value_to_sql_string/1, decode_any_int/0, decode_any_float/0, decoder_of_fexpr_value/1, fexpr_value_compare/2, empty_fexpr/0, fexpr/3, 'and'/4, 'or'/4, and_builder/2, or_builder/2, wrap/1, dual/5, to_string/1, to_sql/1, verify_fexpr/2, verify_fexpr_dyn/2]).
-export_type([fexpr_value/0, operator/0, fexpr_node/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 fexpr_value() :: {string, binary()} |
    {int, integer()} |
    {float, float()} |
    {bool, boolean()} |
    null |
    {date_time, birl:time()} |
    {array, list(fexpr_value())}.

-type operator() :: equal |
    not_equal |
    greater |
    greater_or_equal |
    less |
    less_or_equal |
    contains |
    not_contains |
    at_least_one_equal |
    at_least_one_not_equal |
    at_least_one_greater |
    at_least_one_greater_or_equal |
    at_least_one_less |
    at_least_one_less_or_equal |
    at_least_one_contains |
    at_least_one_not_contains.

-type fexpr_node() :: none |
    {wrap, fexpr_node()} |
    {fexpr, binary(), operator(), fexpr_value()} |
    {or_bin_op, fexpr_node(), fexpr_node()} |
    {and_bin_op, fexpr_node(), fexpr_node()}.

-file("src/fexpr.gleam", 28).
-spec fexpr_value_to_json(fexpr_value()) -> gleam@json:json().
fexpr_value_to_json(Value) ->
    case Value of
        {string, S} ->
            gleam@json:string(S);

        {int, I} ->
            gleam@json:int(I);

        {float, F} ->
            gleam@json:float(F);

        {bool, B} ->
            gleam@json:bool(B);

        null ->
            gleam@json:null();

        {date_time, Dt} ->
            gleam@json:string(birl:to_iso8601(Dt));

        {array, Arr} ->
            _pipe = Arr,
            _pipe@1 = gleam@list:map(
                _pipe,
                fun(Node) -> fexpr_value_to_json(Node) end
            ),
            gleam@json:preprocessed_array(_pipe@1)
    end.

-file("src/fexpr.gleam", 43).
-spec fexpr_value_to_string(fexpr_value()) -> binary().
fexpr_value_to_string(Value) ->
    gleam@json:to_string(fexpr_value_to_json(Value)).

-file("src/fexpr.gleam", 47).
-spec fexpr_value_to_sql_string(fexpr_value()) -> binary().
fexpr_value_to_sql_string(Value) ->
    case Value of
        {string, S} ->
            <<<<"'"/utf8,
                    (gleam@string:replace(S, <<"'"/utf8>>, <<"''"/utf8>>))/binary>>/binary,
                "'"/utf8>>;

        {int, I} ->
            erlang:integer_to_binary(I);

        {float, F} ->
            gleam_stdlib:float_to_string(F);

        {bool, B} when B ->
            <<"1"/utf8>>;

        {bool, _} ->
            <<"0"/utf8>>;

        null ->
            <<"NULL"/utf8>>;

        {date_time, Dt} ->
            erlang:integer_to_binary(birl:to_unix(Dt));

        {array, Arr} ->
            _pipe = Arr,
            _pipe@1 = gleam@list:map(
                _pipe,
                fun(Node) -> fexpr_value_to_sql_string(Node) end
            ),
            _pipe@2 = gleam@string:join(_pipe@1, <<", "/utf8>>),
            (fun(S@1) -> <<<<"("/utf8, S@1/binary>>/binary, ")"/utf8>> end)(
                _pipe@2
            )
    end.

-file("src/fexpr.gleam", 465).
?DOC(" Creates a decoder for the Number type.\n").
-spec decode_any_int() -> gleam@dynamic@decode:decoder(integer()).
decode_any_int() ->
    gleam@dynamic@decode:one_of(
        {decoder, fun gleam@dynamic@decode:decode_int/1},
        [begin
                _pipe = {decoder, fun gleam@dynamic@decode:decode_float/1},
                gleam@dynamic@decode:map(_pipe, fun erlang:round/1)
            end]
    ).

-file("src/fexpr.gleam", 460).
?DOC(" Creates a decoder for the Number type.\n").
-spec decode_any_float() -> gleam@dynamic@decode:decoder(float()).
decode_any_float() ->
    gleam@dynamic@decode:one_of(
        {decoder, fun gleam@dynamic@decode:decode_float/1},
        [begin
                _pipe = {decoder, fun gleam@dynamic@decode:decode_int/1},
                gleam@dynamic@decode:map(_pipe, fun erlang:float/1)
            end]
    ).

-file("src/fexpr.gleam", 64).
-spec decoder_of_fexpr_value(fexpr_value()) -> gleam@dynamic@decode:decoder(fexpr_value()).
decoder_of_fexpr_value(Value) ->
    Base_decoder = case Value of
        {array, Values} ->
            gleam@dynamic@decode:one_of(
                gleam@dynamic@decode:map(
                    gleam@dynamic@decode:list(
                        begin
                            _pipe = gleam@list:first(Values),
                            _pipe@1 = gleam@result:map(
                                _pipe,
                                fun decoder_of_fexpr_value/1
                            ),
                            gleam@result:unwrap(
                                _pipe@1,
                                gleam@dynamic@decode:success(null)
                            )
                        end
                    ),
                    fun(Field@0) -> {array, Field@0} end
                ),
                [begin
                        _pipe@2 = gleam@list:first(Values),
                        _pipe@3 = gleam@result:map(
                            _pipe@2,
                            fun decoder_of_fexpr_value/1
                        ),
                        gleam@result:unwrap(
                            _pipe@3,
                            gleam@dynamic@decode:success(null)
                        )
                    end]
            );

        {bool, _} ->
            gleam@dynamic@decode:map(
                {decoder, fun gleam@dynamic@decode:decode_bool/1},
                fun(Field@0) -> {bool, Field@0} end
            );

        {date_time, _} ->
            gleam@dynamic@decode:then(
                {decoder, fun gleam@dynamic@decode:decode_string/1},
                fun(Value@1) -> case birl:parse(Value@1) of
                        {ok, Dt} ->
                            gleam@dynamic@decode:success({date_time, Dt});

                        {error, E} ->
                            gleam@dynamic@decode:failure(
                                {date_time, birl:now()},
                                <<"Failed to parse date: "/utf8,
                                    (gleam@string:inspect(E))/binary>>
                            )
                    end end
            );

        {float, _} ->
            gleam@dynamic@decode:map(
                decode_any_float(),
                fun(Field@0) -> {float, Field@0} end
            );

        {int, _} ->
            gleam@dynamic@decode:map(
                decode_any_int(),
                fun(Field@0) -> {int, Field@0} end
            );

        null ->
            gleam@dynamic@decode:then(
                gleam@dynamic@decode:optional(
                    {decoder, fun gleam@dynamic@decode:decode_dynamic/1}
                ),
                fun(Value@2) -> case Value@2 of
                        none ->
                            gleam@dynamic@decode:success(null);

                        {some, _} ->
                            gleam@dynamic@decode:failure(
                                null,
                                <<"Expected null"/utf8>>
                            )
                    end end
            );

        {string, _} ->
            gleam@dynamic@decode:map(
                {decoder, fun gleam@dynamic@decode:decode_string/1},
                fun(Field@0) -> {string, Field@0} end
            )
    end,
    gleam@dynamic@decode:one_of(
        gleam@dynamic@decode:map(
            gleam@dynamic@decode:list(Base_decoder),
            fun(Field@0) -> {array, Field@0} end
        ),
        [Base_decoder]
    ).

-file("src/fexpr.gleam", 109).
-spec fexpr_value_compare(fexpr_value(), fexpr_value()) -> {ok,
        gleam@order:order()} |
    {error, nil}.
fexpr_value_compare(A, B) ->
    case {A, B} of
        {{int, A@1}, {int, B@1}} ->
            {ok, gleam@int:compare(A@1, B@1)};

        {{int, A@2}, {float, B@2}} ->
            {ok, gleam@float:compare(erlang:float(A@2), B@2)};

        {{float, A@3}, {int, B@3}} ->
            {ok, gleam@float:compare(A@3, erlang:float(B@3))};

        {{float, A@4}, {float, B@4}} ->
            {ok, gleam@float:compare(A@4, B@4)};

        {{date_time, A@5}, {date_time, B@5}} ->
            case birl@duration:blur_to(birl:difference(A@5, B@5), second) of
                0 ->
                    {ok, eq};

                _ ->
                    {ok, birl:compare(A@5, B@5)}
            end;

        {_, _} ->
            {error, nil}
    end.

-file("src/fexpr.gleam", 152).
?DOC(" Convert the given operator to its string representation.\n").
-spec operator_to_string(operator()) -> binary().
operator_to_string(Operator) ->
    case Operator of
        equal ->
            <<" = "/utf8>>;

        not_equal ->
            <<" != "/utf8>>;

        greater ->
            <<" > "/utf8>>;

        greater_or_equal ->
            <<" >= "/utf8>>;

        less ->
            <<" < "/utf8>>;

        less_or_equal ->
            <<" <= "/utf8>>;

        contains ->
            <<" ~ "/utf8>>;

        not_contains ->
            <<" !~ "/utf8>>;

        at_least_one_equal ->
            <<" ?= "/utf8>>;

        at_least_one_not_equal ->
            <<" ?!= "/utf8>>;

        at_least_one_greater ->
            <<" ?> "/utf8>>;

        at_least_one_greater_or_equal ->
            <<" ?>= "/utf8>>;

        at_least_one_less ->
            <<" ?< "/utf8>>;

        at_least_one_less_or_equal ->
            <<" ?<= "/utf8>>;

        at_least_one_contains ->
            <<" ?~ "/utf8>>;

        at_least_one_not_contains ->
            <<" ?!~ "/utf8>>
    end.

-file("src/fexpr.gleam", 173).
-spec opereator_to_sql_string(operator()) -> binary().
opereator_to_sql_string(Operator) ->
    case Operator of
        equal ->
            <<" = "/utf8>>;

        not_equal ->
            <<" != "/utf8>>;

        greater ->
            <<" > "/utf8>>;

        greater_or_equal ->
            <<" >= "/utf8>>;

        less ->
            <<" < "/utf8>>;

        less_or_equal ->
            <<" <= "/utf8>>;

        contains ->
            <<" LIKE "/utf8>>;

        not_contains ->
            <<" NOT LIKE "/utf8>>;

        at_least_one_equal ->
            <<" IN "/utf8>>;

        at_least_one_not_equal ->
            <<" NOT IN "/utf8>>;

        at_least_one_greater ->
            <<" > ANY "/utf8>>;

        at_least_one_greater_or_equal ->
            <<" >= ANY "/utf8>>;

        at_least_one_less ->
            <<" < ANY "/utf8>>;

        at_least_one_less_or_equal ->
            <<" <= ANY "/utf8>>;

        at_least_one_contains ->
            <<" LIKE ANY "/utf8>>;

        at_least_one_not_contains ->
            <<" NOT LIKE ANY "/utf8>>
    end.

-file("src/fexpr.gleam", 208).
?DOC(" Create an empty fexpr node.\n").
-spec empty_fexpr() -> fexpr_node().
empty_fexpr() ->
    none.

-file("src/fexpr.gleam", 213).
?DOC(" Create a fexpr node with the given field, operator and value.\n").
-spec fexpr(binary(), operator(), fexpr_value()) -> fexpr_node().
fexpr(Field, Operator, Value) ->
    {fexpr, Field, Operator, Value}.

-file("src/fexpr.gleam", 218).
?DOC(" Create a fexpr node that is the logical AND of the given fexpr node and a new fexpr node with the given field, operator and value.\n").
-spec 'and'(fexpr_node(), binary(), operator(), fexpr_value()) -> fexpr_node().
'and'(Fexpr, Field, Operator, Value) ->
    {and_bin_op, Fexpr, {fexpr, Field, Operator, Value}}.

-file("src/fexpr.gleam", 231).
?DOC(" Create a fexpr node that is the logical OR of the given fexpr node and a new fexpr node with the given field, operator and value.\n").
-spec 'or'(fexpr_node(), binary(), operator(), fexpr_value()) -> fexpr_node().
'or'(Fexpr, Field, Operator, Value) ->
    {or_bin_op, Fexpr, {fexpr, Field, Operator, Value}}.

-file("src/fexpr.gleam", 244).
?DOC(" Create a fexpr node that is the logical AND of the two given fexpr nodes.\n").
-spec and_builder(fexpr_node(), fexpr_node()) -> fexpr_node().
and_builder(Fexpr, Other) ->
    {and_bin_op, Fexpr, Other}.

-file("src/fexpr.gleam", 249).
?DOC(" Create a fexpr node that is the logical OR of the two given fexpr nodes.\n").
-spec or_builder(fexpr_node(), fexpr_node()) -> fexpr_node().
or_builder(Fexpr, Other) ->
    {or_bin_op, Fexpr, Other}.

-file("src/fexpr.gleam", 254).
?DOC(" Wrap the given fexpr node in parentheses.\n").
-spec wrap(fexpr_node()) -> fexpr_node().
wrap(Fexpr) ->
    {wrap, Fexpr}.

-file("src/fexpr.gleam", 258).
-spec dual(binary(), binary(), operator(), fexpr_value(), fexpr_value()) -> fexpr_node().
dual(Field_a, Field_b, Operator, Value_a, Value_b) ->
    _pipe = fexpr(Field_a, Operator, Value_a),
    _pipe@1 = 'and'(_pipe, Field_b, Operator, Value_b),
    _pipe@2 = wrap(_pipe@1),
    or_builder(
        _pipe@2,
        begin
            _pipe@3 = fexpr(Field_a, Operator, Value_b),
            _pipe@4 = 'and'(_pipe@3, Field_b, Operator, Value_a),
            wrap(_pipe@4)
        end
    ).

-file("src/fexpr.gleam", 276).
?DOC(" Convert the given fexpr node to its string representation.\n").
-spec to_string(fexpr_node()) -> binary().
to_string(Node) ->
    case Node of
        none ->
            <<""/utf8>>;

        {wrap, Inner} ->
            <<<<"("/utf8, (to_string(Inner))/binary>>/binary, ")"/utf8>>;

        {fexpr, Field, Operator, Value} ->
            <<<<Field/binary, (operator_to_string(Operator))/binary>>/binary,
                (fexpr_value_to_string(Value))/binary>>;

        {and_bin_op, Lhs, Rhs} ->
            <<<<(to_string(Lhs))/binary, "&&"/utf8>>/binary,
                (to_string(Rhs))/binary>>;

        {or_bin_op, Lhs@1, Rhs@1} ->
            <<<<(to_string(Lhs@1))/binary, "||"/utf8>>/binary,
                (to_string(Rhs@1))/binary>>
    end.

-file("src/fexpr.gleam", 287).
-spec to_sql(fexpr_node()) -> binary().
to_sql(Node) ->
    case Node of
        none ->
            <<"1=1"/utf8>>;

        {wrap, Inner} ->
            <<<<"("/utf8, (to_sql(Inner))/binary>>/binary, ")"/utf8>>;

        {fexpr, Field, Operator, Value} ->
            <<<<Field/binary, (opereator_to_sql_string(Operator))/binary>>/binary,
                (fexpr_value_to_sql_string(Value))/binary>>;

        {and_bin_op, Lhs, Rhs} ->
            <<<<(to_sql(Lhs))/binary, " AND "/utf8>>/binary,
                (to_sql(Rhs))/binary>>;

        {or_bin_op, Lhs@1, Rhs@1} ->
            <<<<(to_sql(Lhs@1))/binary, " OR "/utf8>>/binary,
                (to_sql(Rhs@1))/binary>>
    end.

-file("src/fexpr.gleam", 300).
-spec verify_equality(fexpr_value(), operator(), fexpr_value()) -> boolean().
verify_equality(A, Operator, B) ->
    case Operator of
        equal ->
            (A =:= B) orelse (fexpr_value_compare(A, B) =:= {ok, eq});

        not_equal ->
            (A /= B) andalso (fexpr_value_compare(A, B) /= {ok, eq});

        greater ->
            fexpr_value_compare(A, B) =:= {ok, gt};

        greater_or_equal ->
            case fexpr_value_compare(A, B) of
                {ok, gt} ->
                    true;

                {ok, eq} ->
                    true;

                _ ->
                    false
            end;

        less ->
            fexpr_value_compare(A, B) =:= {ok, lt};

        less_or_equal ->
            case fexpr_value_compare(A, B) of
                {ok, lt} ->
                    true;

                {ok, eq} ->
                    true;

                _ ->
                    false
            end;

        contains ->
            case {A, B} of
                {{array, Arr}, _} ->
                    gleam@list:any(
                        Arr,
                        fun(V) -> verify_equality(V, equal, B) end
                    );

                {{string, A@1}, {string, B@1}} ->
                    Free_start = gleam_stdlib:string_starts_with(
                        B@1,
                        <<"%"/utf8>>
                    ),
                    Free_end = gleam_stdlib:string_ends_with(B@1, <<"%"/utf8>>),
                    case {Free_start, Free_end} of
                        {true, true} ->
                            gleam_stdlib:contains_string(
                                A@1,
                                begin
                                    _pipe = B@1,
                                    _pipe@1 = gleam@string:drop_start(_pipe, 1),
                                    gleam@string:drop_end(_pipe@1, 1)
                                end
                            );

                        {true, false} ->
                            gleam_stdlib:string_ends_with(
                                A@1,
                                begin
                                    _pipe@2 = B@1,
                                    gleam@string:drop_start(_pipe@2, 1)
                                end
                            );

                        {false, true} ->
                            gleam_stdlib:string_starts_with(
                                A@1,
                                begin
                                    _pipe@3 = B@1,
                                    gleam@string:drop_end(_pipe@3, 1)
                                end
                            );

                        {false, false} ->
                            verify_equality({string, A@1}, equal, {string, B@1})
                    end;

                {_, _} ->
                    false
            end;

        not_contains ->
            not verify_equality(A, contains, B);

        at_least_one_equal ->
            case {A, B} of
                {A@2, {array, Arr_b}} ->
                    gleam@list:any(
                        Arr_b,
                        fun(V@1) -> verify_equality(V@1, equal, A@2) end
                    );

                {_, _} ->
                    false
            end;

        at_least_one_not_equal ->
            case {A, B} of
                {A@3, {array, Arr_b@1}} ->
                    gleam@list:any(
                        Arr_b@1,
                        fun(V@2) -> verify_equality(V@2, not_equal, A@3) end
                    );

                {_, _} ->
                    false
            end;

        at_least_one_greater ->
            case {A, B} of
                {A@4, {array, Arr_b@2}} ->
                    gleam@list:any(
                        Arr_b@2,
                        fun(V@3) -> verify_equality(V@3, greater, A@4) end
                    );

                {_, _} ->
                    false
            end;

        at_least_one_greater_or_equal ->
            case {A, B} of
                {A@5, {array, Arr_b@3}} ->
                    gleam@list:any(
                        Arr_b@3,
                        fun(V@4) ->
                            verify_equality(V@4, greater_or_equal, A@5)
                        end
                    );

                {_, _} ->
                    false
            end;

        at_least_one_less ->
            case {A, B} of
                {A@6, {array, Arr_b@4}} ->
                    gleam@list:any(
                        Arr_b@4,
                        fun(V@5) -> verify_equality(V@5, less, A@6) end
                    );

                {_, _} ->
                    false
            end;

        at_least_one_less_or_equal ->
            case {A, B} of
                {A@7, {array, Arr_b@5}} ->
                    gleam@list:any(
                        Arr_b@5,
                        fun(V@6) -> verify_equality(V@6, less_or_equal, A@7) end
                    );

                {_, _} ->
                    false
            end;

        at_least_one_contains ->
            case {A, B} of
                {A@8, {array, Arr_b@6}} ->
                    gleam@list:any(
                        Arr_b@6,
                        fun(V@7) -> verify_equality(A@8, contains, V@7) end
                    );

                {_, _} ->
                    false
            end;

        at_least_one_not_contains ->
            case {A, B} of
                {A@9, {array, Arr_b@7}} ->
                    gleam@list:any(
                        Arr_b@7,
                        fun(V@8) -> verify_equality(A@9, not_contains, V@8) end
                    );

                {_, _} ->
                    false
            end
    end.

-file("src/fexpr.gleam", 387).
-spec verify_fexpr(fexpr_node(), binary()) -> boolean().
verify_fexpr(Fexpr, Data) ->
    case Fexpr of
        {fexpr, Field, Operator, Value} ->
            Parts = gleam@string:split(Field, <<"."/utf8>>),
            Decoded = gleam@json:parse(
                Data,
                begin
                    gleam@dynamic@decode:subfield(
                        Parts,
                        decoder_of_fexpr_value(Value),
                        fun(Value@1) ->
                            gleam@dynamic@decode:success(Value@1)
                        end
                    )
                end
            ),
            case Decoded of
                {ok, Decoded@1} ->
                    verify_equality(Decoded@1, Operator, Value);

                {error, {unable_to_decode, Errors}} when Value =:= null ->
                    gleam@list:any(
                        Errors,
                        fun(Error) ->
                            (erlang:element(2, Error) =:= <<"Field"/utf8>>)
                            andalso (erlang:element(3, Error) =:= <<"Nothing"/utf8>>)
                        end
                    );

                {error, _} ->
                    false
            end;

        none ->
            true;

        {and_bin_op, Left, Right} ->
            A = verify_fexpr(Left, Data),
            B = verify_fexpr(Right, Data),
            A andalso B;

        {or_bin_op, Left@1, Right@1} ->
            A@1 = verify_fexpr(Left@1, Data),
            B@1 = verify_fexpr(Right@1, Data),
            A@1 orelse B@1;

        {wrap, Inner} ->
            verify_fexpr(Inner, Data)
    end.

-file("src/fexpr.gleam", 423).
-spec verify_fexpr_dyn(fexpr_node(), gleam@dynamic:dynamic_()) -> boolean().
verify_fexpr_dyn(Fexpr, Data) ->
    case Fexpr of
        {fexpr, Field, Operator, Value} ->
            Parts = gleam@string:split(Field, <<"."/utf8>>),
            Decoded = gleam@dynamic@decode:run(
                Data,
                begin
                    gleam@dynamic@decode:subfield(
                        Parts,
                        decoder_of_fexpr_value(Value),
                        fun(Value@1) ->
                            gleam@dynamic@decode:success(Value@1)
                        end
                    )
                end
            ),
            case Decoded of
                {ok, Decoded@1} ->
                    verify_equality(Decoded@1, Operator, Value);

                {error, Errors} when Value =:= null ->
                    gleam@list:any(
                        Errors,
                        fun(Error) ->
                            (erlang:element(2, Error) =:= <<"Field"/utf8>>)
                            andalso (erlang:element(3, Error) =:= <<"Nothing"/utf8>>)
                        end
                    );

                {error, _} ->
                    false
            end;

        none ->
            true;

        {and_bin_op, Left, Right} ->
            A = verify_fexpr_dyn(Left, Data),
            B = verify_fexpr_dyn(Right, Data),
            A andalso B;

        {or_bin_op, Left@1, Right@1} ->
            A@1 = verify_fexpr_dyn(Left@1, Data),
            B@1 = verify_fexpr_dyn(Right@1, Data),
            A@1 orelse B@1;

        {wrap, Inner} ->
            verify_fexpr_dyn(Inner, Data)
    end.