Skip to main content

src/fexpr.gleam

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)])
}