import birl
import birl/duration
import gleam/dynamic
import gleam/dynamic/decode
import gleam/float
import gleam/int
import gleam/json
import gleam/list
import gleam/option
import gleam/order
import gleam/result
import gleam/string
//-----------------------------------------------------------------------------------------------//
// fexpr Value //
//-----------------------------------------------------------------------------------------------//
pub type FexprValue {
String(String)
Int(Int)
Float(Float)
Bool(Bool)
Null
DateTime(birl.Time)
Array(List(FexprValue))
}
pub fn fexpr_value_to_json(value: FexprValue) -> json.Json {
case value {
String(s) -> json.string(s)
Int(i) -> json.int(i)
Float(f) -> json.float(f)
Bool(b) -> json.bool(b)
Null -> json.null()
DateTime(dt) -> json.string(birl.to_iso8601(dt))
Array(arr) ->
arr
|> list.map(fn(node) { fexpr_value_to_json(node) })
|> json.preprocessed_array
}
}
pub fn fexpr_value_to_string(value: FexprValue) -> String {
json.to_string(fexpr_value_to_json(value))
}
pub fn fexpr_value_to_sql_string(value: FexprValue) -> String {
case value {
String(s) -> "'" <> string.replace(s, "'", "''") <> "'"
Int(i) -> int.to_string(i)
Float(f) -> float.to_string(f)
Bool(b) if b -> "1"
Bool(_) -> "0"
Null -> "NULL"
DateTime(dt) -> int.to_string(birl.to_unix(dt))
Array(arr) ->
arr
|> list.map(fn(node) { fexpr_value_to_sql_string(node) })
|> string.join(", ")
|> fn(s) { "(" <> s <> ")" }
}
}
pub fn decoder_of_fexpr_value(value: FexprValue) -> decode.Decoder(FexprValue) {
let base_decoder = case value {
Array(values) ->
decode.one_of(
decode.map(
decode.list(
list.first(values)
|> result.map(decoder_of_fexpr_value)
|> result.unwrap(decode.success(Null)),
),
Array,
),
[
list.first(values)
|> result.map(decoder_of_fexpr_value)
|> result.unwrap(decode.success(Null)),
],
)
Bool(_) -> decode.map(decode.bool, Bool)
DateTime(_) ->
decode.then(decode.string, fn(value) {
case birl.parse(value) {
Ok(dt) -> decode.success(DateTime(dt))
Error(e) ->
decode.failure(
DateTime(birl.now()),
"Failed to parse date: " <> string.inspect(e),
)
}
})
Float(_) -> decode.map(decode_any_float(), Float)
Int(_) -> decode.map(decode_any_int(), Int)
Null ->
decode.then(decode.optional(decode.dynamic), fn(value) {
case value {
option.None -> decode.success(Null)
option.Some(_) -> decode.failure(Null, "Expected null")
}
})
String(_) -> decode.map(decode.string, String)
}
decode.one_of(decode.map(decode.list(base_decoder), Array), [base_decoder])
}
pub fn fexpr_value_compare(
a: FexprValue,
b: FexprValue,
) -> Result(order.Order, Nil) {
case a, b {
Int(a), Int(b) -> Ok(int.compare(a, b))
Int(a), Float(b) -> Ok(float.compare(int.to_float(a), b))
Float(a), Int(b) -> Ok(float.compare(a, int.to_float(b)))
Float(a), Float(b) -> Ok(float.compare(a, b))
DateTime(a), DateTime(b) ->
case duration.blur_to(birl.difference(a, b), duration.Second) {
0 -> Ok(order.Eq)
_ -> Ok(birl.compare(a, b))
}
_, _ -> Error(Nil)
}
}
//-----------------------------------------------------------------------------------------------//
// Operator //
//-----------------------------------------------------------------------------------------------//
/// The operators that can be used in a (pocketbase) fexpr expression.
pub type Operator {
Equal
NotEqual
Greater
GreaterOrEqual
Less
LessOrEqual
Contains
NotContains
AtLeastOneEqual
AtLeastOneNotEqual
AtLeastOneGreater
AtLeastOneGreaterOrEqual
AtLeastOneLess
AtLeastOneLessOrEqual
AtLeastOneContains
AtLeastOneNotContains
}
/// Convert the given operator to its string representation.
fn operator_to_string(operator: Operator) -> String {
case operator {
Equal -> " = "
NotEqual -> " != "
Greater -> " > "
GreaterOrEqual -> " >= "
Less -> " < "
LessOrEqual -> " <= "
Contains -> " ~ "
NotContains -> " !~ "
AtLeastOneEqual -> " ?= "
AtLeastOneNotEqual -> " ?!= "
AtLeastOneGreater -> " ?> "
AtLeastOneGreaterOrEqual -> " ?>= "
AtLeastOneLess -> " ?< "
AtLeastOneLessOrEqual -> " ?<= "
AtLeastOneContains -> " ?~ "
AtLeastOneNotContains -> " ?!~ "
}
}
fn opereator_to_sql_string(operator: Operator) -> String {
case operator {
Equal -> " = "
NotEqual -> " != "
Greater -> " > "
GreaterOrEqual -> " >= "
Less -> " < "
LessOrEqual -> " <= "
Contains -> " LIKE "
NotContains -> " NOT LIKE "
AtLeastOneEqual -> " IN "
AtLeastOneNotEqual -> " NOT IN "
AtLeastOneGreater -> " > ANY "
AtLeastOneGreaterOrEqual -> " >= ANY "
AtLeastOneLess -> " < ANY "
AtLeastOneLessOrEqual -> " <= ANY "
AtLeastOneContains -> " LIKE ANY "
AtLeastOneNotContains -> " NOT LIKE ANY "
}
}
//-----------------------------------------------------------------------------------------------//
// fexpr Node //
//-----------------------------------------------------------------------------------------------//
/// A node in a fexpr expression tree.
pub type FexprNode {
None
Wrap(FexprNode)
Fexpr(field: String, operator: Operator, value: FexprValue)
OrBinOp(left: FexprNode, right: FexprNode)
AndBinOp(left: FexprNode, right: FexprNode)
}
/// Create an empty fexpr node.
pub fn empty_fexpr() -> FexprNode {
None
}
/// Create a fexpr node with the given field, operator and value.
pub fn fexpr(field: String, operator: Operator, value: FexprValue) -> FexprNode {
Fexpr(field: field, operator: operator, value: value)
}
/// 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.
pub fn and(
fexpr: FexprNode,
field: String,
operator: Operator,
value: FexprValue,
) -> FexprNode {
AndBinOp(
left: fexpr,
right: Fexpr(field: field, operator: operator, value: value),
)
}
/// 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.
pub fn or(
fexpr: FexprNode,
field: String,
operator: Operator,
value: FexprValue,
) -> FexprNode {
OrBinOp(
left: fexpr,
right: Fexpr(field: field, operator: operator, value: value),
)
}
/// Create a fexpr node that is the logical AND of the two given fexpr nodes.
pub fn and_builder(fexpr: FexprNode, other: FexprNode) -> FexprNode {
AndBinOp(left: fexpr, right: other)
}
/// Create a fexpr node that is the logical OR of the two given fexpr nodes.
pub fn or_builder(fexpr: FexprNode, other: FexprNode) -> FexprNode {
OrBinOp(left: fexpr, right: other)
}
/// Wrap the given fexpr node in parentheses.
pub fn wrap(fexpr: FexprNode) -> FexprNode {
Wrap(fexpr)
}
pub fn dual(
field_a: String,
field_b: String,
operator: Operator,
value_a: FexprValue,
value_b: FexprValue,
) {
fexpr(field_a, operator, value_a)
|> and(field_b, operator, value_b)
|> wrap()
|> or_builder(
fexpr(field_a, operator, value_b)
|> and(field_b, operator, value_a)
|> wrap(),
)
}
/// Convert the given fexpr node to its string representation.
pub fn to_string(node: FexprNode) -> String {
case node {
None -> ""
Wrap(inner) -> "(" <> to_string(inner) <> ")"
Fexpr(field, operator, value) ->
field <> operator_to_string(operator) <> fexpr_value_to_string(value)
AndBinOp(lhs, rhs) -> to_string(lhs) <> "&&" <> to_string(rhs)
OrBinOp(lhs, rhs) -> to_string(lhs) <> "||" <> to_string(rhs)
}
}
pub fn to_sql(node: FexprNode) -> String {
case node {
None -> "1=1"
Wrap(inner) -> "(" <> to_sql(inner) <> ")"
Fexpr(field, operator, value) ->
field
<> opereator_to_sql_string(operator)
<> fexpr_value_to_sql_string(value)
AndBinOp(lhs, rhs) -> to_sql(lhs) <> " AND " <> to_sql(rhs)
OrBinOp(lhs, rhs) -> to_sql(lhs) <> " OR " <> to_sql(rhs)
}
}
fn verify_equality(a: FexprValue, operator: Operator, b: FexprValue) {
case operator {
Equal -> a == b || fexpr_value_compare(a, b) == Ok(order.Eq)
NotEqual -> a != b && fexpr_value_compare(a, b) != Ok(order.Eq)
Greater -> fexpr_value_compare(a, b) == Ok(order.Gt)
GreaterOrEqual ->
case fexpr_value_compare(a, b) {
Ok(order.Gt) | Ok(order.Eq) -> True
_ -> False
}
Less -> fexpr_value_compare(a, b) == Ok(order.Lt)
LessOrEqual ->
case fexpr_value_compare(a, b) {
Ok(order.Lt) | Ok(order.Eq) -> True
_ -> False
}
Contains ->
case a, b {
Array(arr), _ -> list.any(arr, fn(v) { verify_equality(v, Equal, b) })
String(a), String(b) -> {
let free_start = string.starts_with(b, "%")
let free_end = string.ends_with(b, "%")
case free_start, free_end {
True, True ->
string.contains(
a,
b |> string.drop_start(1) |> string.drop_end(1),
)
True, False -> string.ends_with(a, b |> string.drop_start(1))
False, True -> string.starts_with(a, b |> string.drop_end(1))
False, False -> verify_equality(String(a), Equal, String(b))
}
}
_, _ -> False
}
NotContains -> !verify_equality(a, Contains, b)
AtLeastOneEqual ->
case a, b {
a, Array(arr_b) ->
list.any(arr_b, fn(v) { verify_equality(v, Equal, a) })
_, _ -> False
}
AtLeastOneNotEqual ->
case a, b {
a, Array(arr_b) ->
list.any(arr_b, fn(v) { verify_equality(v, NotEqual, a) })
_, _ -> False
}
AtLeastOneGreater ->
case a, b {
a, Array(arr_b) ->
list.any(arr_b, fn(v) { verify_equality(v, Greater, a) })
_, _ -> False
}
AtLeastOneGreaterOrEqual ->
case a, b {
a, Array(arr_b) ->
list.any(arr_b, fn(v) { verify_equality(v, GreaterOrEqual, a) })
_, _ -> False
}
AtLeastOneLess ->
case a, b {
a, Array(arr_b) ->
list.any(arr_b, fn(v) { verify_equality(v, Less, a) })
_, _ -> False
}
AtLeastOneLessOrEqual ->
case a, b {
a, Array(arr_b) ->
list.any(arr_b, fn(v) { verify_equality(v, LessOrEqual, a) })
_, _ -> False
}
AtLeastOneContains ->
case a, b {
a, Array(arr_b) ->
list.any(arr_b, fn(v) { verify_equality(a, Contains, v) })
_, _ -> False
}
AtLeastOneNotContains ->
case a, b {
a, Array(arr_b) ->
list.any(arr_b, fn(v) { verify_equality(a, NotContains, v) })
_, _ -> False
}
}
}
pub fn verify_fexpr(fexpr: FexprNode, data: String) -> Bool {
case fexpr {
Fexpr(field:, operator:, value:) -> {
let parts = string.split(field, ".")
let decoded =
json.parse(data, {
use value <- decode.subfield(parts, decoder_of_fexpr_value(value))
decode.success(value)
})
case decoded {
Ok(decoded) -> verify_equality(decoded, operator, value)
Error(json.UnableToDecode(errors)) if value == Null -> {
list.any(errors, fn(error) {
error.expected == "Field" && error.found == "Nothing"
})
}
Error(_) -> False
}
}
None -> True
AndBinOp(left:, right:) -> {
let a = verify_fexpr(left, data)
let b = verify_fexpr(right, data)
a && b
}
OrBinOp(left:, right:) -> {
let a = verify_fexpr(left, data)
let b = verify_fexpr(right, data)
a || b
}
Wrap(inner) -> verify_fexpr(inner, data)
}
}
pub fn verify_fexpr_dyn(fexpr: FexprNode, data: dynamic.Dynamic) -> Bool {
case fexpr {
Fexpr(field:, operator:, value:) -> {
let parts = string.split(field, ".")
let decoded =
decode.run(data, {
use value <- decode.subfield(parts, decoder_of_fexpr_value(value))
decode.success(value)
})
case decoded {
Ok(decoded) -> verify_equality(decoded, operator, value)
Error(errors) if value == Null -> {
list.any(errors, fn(error) {
error.expected == "Field" && error.found == "Nothing"
})
}
Error(_) -> False
}
}
None -> True
AndBinOp(left:, right:) -> {
let a = verify_fexpr_dyn(left, data)
let b = verify_fexpr_dyn(right, data)
a && b
}
OrBinOp(left:, right:) -> {
let a = verify_fexpr_dyn(left, data)
let b = verify_fexpr_dyn(right, data)
a || b
}
Wrap(inner) -> verify_fexpr_dyn(inner, data)
}
}
/// Creates a decoder for the Number type.
pub fn decode_any_float() -> decode.Decoder(Float) {
decode.one_of(decode.float, [decode.int |> decode.map(int.to_float)])
}
/// Creates a decoder for the Number type.
pub fn decode_any_int() -> decode.Decoder(Int) {
decode.one_of(decode.int, [decode.float |> decode.map(float.round)])
}