# Copyright 2023 Arkemis S.r.l.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
defmodule Arke.Core.Query do
@moduledoc """
Struct which defines a Query
"""
defstruct ~w[project arke persistence filters link orders offset limit]a
@type t() :: %Arke.Core.Query{}
defmodule LinkFilter do
@moduledoc """
Base struct of a LinkFilter:
- unit => %Arke.Core.`{arke_struct}`{} => the `arke_struct` of the unit which we want to filter on. See `Arke.Struct`
- depth => integer => how many results we want to have at max
- direction => "child" | "parent" => the direction the query will use to search,
- type => the name of the connection we want to look at \n
It is used to define a common filter struct which will be applied on an arke_link Query
"""
defstruct ~w[unit depth direction type]a
@type t() :: %Arke.Core.Query.LinkFilter{}
end
defmodule Filter do
@moduledoc """
Base struct of a Filter:
- logic => :and | :or => the logic of the filter
- negate => boolean => used to figure out whether the condition is to be denied
- base_filters (refer to `Arke.Core.Query.BaseFilter`).\n
It is used to define a Filter which will be applied on a Query
"""
defstruct ~w[logic negate base_filters]a
@type t() :: %Arke.Core.Query.Filter{}
end
defmodule BaseFilter do
@moduledoc """
Base struct of a BaseFilter:
- parameter => %Arke.Core.Parameter.`ParameterType` => refer to `Arke.Core.Parameter`
- operator => refer to [operators](#module-operators)
- value => any => the value that the query will search for
- negate => boolean => used to figure out whether the condition is to be denied \n
It is used to keep the same logic structure across all the Filter
"""
defstruct ~w[parameter operator value negate]a
@type t() :: %Arke.Core.Query.BaseFilter{}
@doc """
Create a new BaseParameter
## Parameters
- parameter => %Arke.Core.Parameter.`ParameterType` => refer to `Arke.Core.Parameter`
- operator => refer to [operators](#module-operators)
- value => any => the value that the query will search for
- negate => boolean => used to figure out whether the condition is to be denied \n
## Example
iex> filter = Arke.Core.Query.BaseFilter.new(parameter: "name", operator: "eq", value: "John", negate: false)
...> Arke.Core.Query.BaseFilter.new(person, :default)
## Return
%Arke.Core.Query.BaseFilter{}
"""
@spec new(
parameter :: Arke.Core.Parameter.ParameterType,
operator :: atom(),
value :: any,
negate :: boolean
) :: Arke.Core.Query.BaseFilter.t()
def new(parameter, operator, value, negate) do
%__MODULE__{
parameter: parameter,
operator: operator,
value: cast_value(parameter, value),
negate: negate
}
end
defp cast_value(%Arke.Core.Unit{arke_id: arke_id} = parameter, value) do
case arke_id do
:datetime ->
Arke.DatetimeHandler.parse_datetime(value)
|> case do
{:ok, value} -> value
_ -> raise ArgumentError, message: "Invalid datetime format"
end
_ ->
value
end
end
defp cast_value(parameter, value), do: value
end
defmodule Order do
@moduledoc """
Base struct Order:
- parameter => %Arke.Core.Parameter.`ParameterType` => refer to `Arke.Core.Parameter`
- direction => "child" | "parent" => the direction the query will use to search \n
It is used to define the return order of a Query
"""
defstruct ~w[parameter direction]a
@type t() :: %Arke.Core.Query.Order{}
end
@doc """
Create a new Query
## Parameters
- arke => %Arke.Core.`{arke_struct}`{} => the `arke_struct` of the unit which we want to filter on. See `Arke.Struct`
- project => :atom => identify the `Arke.Core.Project`
## Example
iex> person = Arke.Core.Arke.new(id: "person", label: "Person")
...> Arke.Core.Query.new(person, :default)
## Return
%Arke.Core.Query{}
"""
@spec new(arke :: %Arke.Core.Arke{}, project :: atom()) :: Arke.Core.Query.t()
def new(arke, project),
do: %__MODULE__{
project: project,
arke: arke,
persistence: nil,
filters: [],
orders: [],
offset: nil,
limit: nil
}
@doc """
Add a new link filter
## Parameters
- query => refer to `new/1`
- unit => %Arke.Core.`{arke_struct}`{} => the `arke_struct` of the unit which we want to filter on. See `Arke.Struct`
- depth => integer => how many results we want to have at max
- direction => "child" | "parent" => the direction the query will use to search
- type => the name of the link we want to look at
## Example
iex> person = Arke.Core.Arke.new(id: :person, label: "Person")
...> query = Arke.Core.Query.new(person, :arke_system)
...> Arke.Core.Query.add_link_filter(query, person, 0, "child", "link")
## Return
%Arke.Core.Query{... link: %Arke.Core.Query.LinkFilter{} ... }
"""
@spec add_link_filter(
query :: Arke.Core.Query.t(),
unit :: Arke.Core.Unit.t(),
depth :: integer(),
direction :: atom(),
connection_type :: String.t()
) :: Arke.Core.Query.t()
def add_link_filter(query, unit, depth, direction, type) do
%{query | link: %LinkFilter{unit: unit, depth: depth, direction: direction, type: type}}
end
@doc """
Add a filter to a query
## Parameters
- query => refer to `new/1`
- filter => refer to `Arke.Core.Query.BaseFilter`
## Example
iex> person = Arke.Core.Arke.new(id: :person, label: "Person")
...> query = Arke.Core.Query.new(person, :arke_system)
...> parameter = Arke.Boundary.ParameterManager.get(:id,:arke_system)
...> filter = Arke.Core.Query.new_filter(parameter,:eq, "name", false)
...> Arke.Core.Query.add_filter(query, filter)
## Return
%Arke.Core.Query{... filters: [ %Arke.Core.Query.Filter{} ] ... }
"""
def add_filter(query, filter) do
%{query | filters: [filter | query.filters]}
end
@doc """
Add a filter to a query
## Parameters
- query => refer to `new/1`
- parameter
- operator
- value
- negate
## Example
iex> person = Arke.Core.Arke.new(id: :person, label: "Person")
...> query = Arke.Core.Query.new(person, :arke_system)
...> parameter = Arke.Boundary.ParameterManager.get(:id,:arke_system)
...> base_filter = Arke.Core.Query.new_base_filter(parameter, :eq, "name", false)
...> Arke.Core.Query.add_filter(query, :and, false, base_filter)
## Return
%Arke.Core.Query{... filters: [ %Arke.Core.Query.Filter{} ] ... }
"""
def add_filter(query, parameter, operator, value, negate) do
%{query | filters: [new_filter(parameter, operator, value, negate) | query.filters]}
end
@doc """
Add a filter to a query
## Parameters
- query => refer to `new/1`
- logic => :and | :or => the logic of the filter
- negate => boolean => used to figure out whether the condition is to be denied
- base_filters
## Example
iex> person = Arke.new(id: :person, label: "Person")
...> query = Arke.Core.Query.new(person, :arke_system)
...> parameter = Arke.Core.ParameterManager.get(:id,:arke_system)
...> Arke.Core.Query.add_filter(query, parameter, :eq, "name", false)
## Return
%Arke.Core.Query{... filters: [ %Arke.Core.Query.Filter{} ] ... }
"""
def add_filter(query, logic, negate, base_filters) do
%{query | filters: [new_filter(logic, negate, base_filters) | query.filters]}
end
@doc """
Create a new filter
## Parameters
- parameter => %Arke.Core.Parameter.`ParameterType` => refer to `Arke.Core.Parameter`
- operator => refer to [operators](#module-operators)
- value => any => the value that the query will search for
- negate => boolean => used to figure out whether the condition is to be denied
## Example
iex> parameter = Arke.Boundary.ParameterManager.get(:id,:arke_system)
...> Arke.Core.Query.new_filter(parameter,:eq, "name", false)
## Return
%Arke.Core.Query.Filter{base_filters: [ %Arke.Core.Query.BaseFilter{} ]}
"""
def new_filter(parameter, operator, value, negate) do
%Filter{
logic: :and,
negate: false,
base_filters: [new_base_filter(parameter, operator, value, negate)]
}
end
@doc """
Create a new filter
## Parameters
- logic => :and | :or => the logic of the filter
- negate => boolean => used to figure out whether the condition is to be denied
- base_filters => refer to `Arke.Core.Query.BaseFilter`
## Example
iex> base_filter = Arke.Core.Query.new_base_filter(parameter, :eq, "name", false)
...> Arke.Core.Query.new_filter(:and, false, base_filter)
## Return
%Arke.Core.Query.Filter{base_filters: [ %Arke.Core.Query.BaseFilter{} ]}
"""
def new_filter(logic, negate, base_filters) do
%Filter{base_filters: parse_base_filters(base_filters), logic: logic, negate: negate}
end
@doc """
Create a new base filter
## Parameters
- parameter => %Arke.Core.Parameter.`ParameterType` => refer to `Arke.Core.Parameter`
- operator => refer to [operators](#module-operators)
- value => any => the value that the query will search for
- negate => boolean => used to figure out whether the condition is to be denied
## Example
iex> parameter = Arke.Boundary.ParameterManager.get(:id,:arke_system)
...> Arke.Core.Query.new_base_filter(parameter, :eq, "name", false)
## Return
%Arke.Core.Query.BaseFilter{}
"""
# TODO: standardize parameter
# if it is a string convert it to existing atom and get it from paramater manager
# if it is an atom get it from paramater manaager
def new_base_filter(parameter, operator, value, negate) do
BaseFilter.new(parameter, operator, value, negate)
end
defp parse_base_filters(base_filters) when is_list(base_filters), do: base_filters
defp parse_base_filters(base_filters), do: [base_filters]
@doc """
Get the query result ordered by specific criteria
## Parameters
- query => refer to refer to `new/1`
- parameter => %Arke.Core.Parameter.`ParameterType` => refer to `Arke.Core.Parameter`
- direction => "child" | "parent" => the direction the query will use to search
## Example
iex> person = Arke.Core.Arke.new(id: "person", label: "Person")
...> query = Arke.Core.Query.new(person, :arke_system)
...> parameter = Arke.Boundary.ParameterManager.get(:id,:arke_system)
...> Arke.Core.Query.add_order(query, parameter, :asc)
## Return
%Arke.Core.Query{ ... orders: [ %Arke.Core.Query.Order{} ] ... }
"""
def add_order(query, parameter, direction) do
%{
query
| orders: [%Order{parameter: parameter, direction: direction} | query.orders]
}
end
@doc """
Define the offset of the query
## Parameters
- query => refer to `new/1`
- offset => integer => define the offset of the query
## Example
iex> person = Arke.Core.Arke.new(id: :person, label: "Person")
...> query = Arke.Core.Query.new(person, :arke_system)
...> Arke.Core.Query.set_offset(query, 5)
## Return
%Arke.Core.Query{... offset: value ...}
"""
def set_offset(query, nil), do: query
def set_offset(query, offset) when is_binary(offset),
do: %{query | offset: String.to_integer(offset)}
def set_offset(query, offset) when is_integer(offset), do: %{query | offset: offset}
# TODO Custom exception offset must be integer
def set_offset(query, offset), do: nil
@doc """
Define the limit of the query
## Parameters
- query => refer to `new/1`
- limit => integer => set the results limit of the query
## Example
iex> person = Arke.Core.Arke.new(id: :person, label: "Person")
...> query = Arke.Core.Query.new(person, :arke_system)
...> Arke.Core.Query.set_limit(query, 100)
## Return
%Arke.Core.Query{... limit: value ...}
"""
def set_limit(query, nil), do: query
def set_limit(query, offset) when is_binary(offset),
do: %{query | limit: String.to_integer(offset)}
def set_limit(query, limit) when is_integer(limit), do: %{query | limit: limit}
# TODO Custom exception limit must be integer
def set_limit(query, limit), do: nil
end