class Magiq::Query

Attributes

model[R]
params[R]
raw_params[R]
scope[R]
solo_param[R]

Public Class Methods

apply(*params, &block) click to toggle source
# File lib/magiq/query.rb, line 61
def self.apply(*params, &block)
  opts = params.last.is_a?(Hash) ? params.pop : {}
  builder.add_listener(:apply, params, opts, &block)
end
builder() click to toggle source
# File lib/magiq/query.rb, line 8
def self.builder
  @builder ||= Builder.new
end
by(column, opts = {}, &block) click to toggle source
# File lib/magiq/query.rb, line 154
def self.by(column, opts = {}, &block)
  param(column, {
    solo:  true,
    type:  :id,
    alias: Magiq::Utils.pluralize(column.to_s).to_sym,
    array: :always
  }.merge(opts))

  if block_given?
    apply(column, &block)
  else
    apply(column) do |ids|
      if ids.empty?
        nil
      else
        tbl = model.table_name

        sql = ids.each_with_index.map { |raw_id, i|
          id = raw_id.is_a?(Numeric) ? raw_id : "'#{raw_id}'"
          "WHEN #{id} THEN #{i}"
        }.join(' ')

        scope.where(column => ids).order("CASE #{tbl}.#{column} #{sql} END")
      end
    end
  end
end
check(*params, &block) click to toggle source
# File lib/magiq/query.rb, line 66
def self.check(*params, &block)
  opts = params.last.is_a?(Hash) ? params.pop : {}
  builder.add_listener(:check, params, opts, &block)
end
def_param(key, opts = {}) click to toggle source
# File lib/magiq/query.rb, line 57
def self.def_param(key, opts = {})
  builder.add_param(key, opts)
end
exclusive(*params) click to toggle source
# File lib/magiq/query.rb, line 75
def self.exclusive(*params)
  builder.add_constraint(:exclusive, params)
end
has_pagination(opts = {}) click to toggle source
# File lib/magiq/query.rb, line 79
def self.has_pagination(opts = {})
  max_page_size     = opts[:max_page_size] || Magiq[:max_page_size]
  min_page_size     = opts[:min_page_size] || Magiq[:min_page_size]
  default_page_size = opts[:default_page_size] || Magiq[:default_page_size]

  param :page,      type: :whole
  param :page_size, type: :whole

  check :page, :page_size, any: true do |page, page_size|
    if page && page < 1
      bad! "The value provided for `page` must be 1 or greater, but " \
      "#{page.inspect} was provided."
    end

    if page_size && page_size > max_page_size
      bad! "The maximum permitted value for `page_size` is " \
      "#{max_page_size}, but #{page_size.inspect} was provided."
    elsif page_size && page_size < min_page_size
      bad! "The minimum permitted value for `page_size` is " \
      "#{min_page_size}, but #{page_size.inspect} was provided."
    end
  end

  apply do
    next if solo?

    page      = params[:page]
    page_size = params[:page_size] || default_page_size
    new_scope = scope.page(page)

    page_size ? new_scope.per(page_size) : new_scope
  end
end
model(&block) click to toggle source
# File lib/magiq/query.rb, line 12
def self.model(&block)
  @model_proc = block
end
model_proc() click to toggle source
# File lib/magiq/query.rb, line 16
def self.model_proc
  @model_proc
end
mutual(params, opts = {}) click to toggle source
# File lib/magiq/query.rb, line 71
def self.mutual(params, opts = {})
  builder.add_constraint(:mutual, params, opts)
end
new(params) click to toggle source
# File lib/magiq/query.rb, line 271
def initialize(params)
  @raw_params = params
  @listeners  = {}
end
param(*args, &block) click to toggle source
# File lib/magiq/query.rb, line 53
def self.param(*args, &block)
  params(*args, &block)
end
params(*keys, &block) click to toggle source
# File lib/magiq/query.rb, line 36
def self.params(*keys, &block)
  opts = keys.last.is_a?(Hash) ? keys.pop : {}

  if block_given?
    types = opts.delete(:type)

    keys.each do |k|
      type = types.is_a?(Hash) ? types[k] : types
      def_param(k, opts.merge(type: type))
    end

    apply(*keys, &block)
  else
    def_param(keys[0], opts)
  end
end
range(field, opts = {}) click to toggle source
# File lib/magiq/query.rb, line 182
def self.range(field, opts = {})
  lt_param  = :"#{field}_lt"
  lte_param = :"#{field}_lte"
  gt_param  = :"#{field}_gt"
  gte_param = :"#{field}_gte"
  eq_param  = field.to_sym

  param(lt_param,  type: opts[:type] || :whole)
  param(lte_param, type: opts[:type] || :whole)
  param(gt_param,  type: opts[:type] || :whole)
  param(gte_param, type: opts[:type] || :whole)
  param(eq_param,  type: opts[:type] || :whole)

  exclusive(gt_param, gte_param)
  exclusive(lt_param, lte_param)
  mutual(eq_param, exclusive: [lt_param, lte_param, gt_param, gte_param])

  check do
    next if Magiq::Utils.present?(params[eq_param])

    lt_par = if (lt_val = params[lte_param])
      lte_param
    elsif (lt_val = params[lt_param])
      lt_param
    end

    gt_par = if (gt_val = params[gte_param])
      gte_param
    elsif (gt_val = params[gt_param])
      gt_param
    end

    next unless lt_par && gt_par

    if lt_val > gt_val
      bad! "A value of #{lt_val} was provided for `#{lt_par}` but a value " \
      "of #{gt_val} was provided for `#{gt_par}`. The permitted value of  " \
      "`#{lt_par}` must be less than the permitted value provided for " \
      "`#{gt_par}`."
    end

    if lt_val == gt_val
      bad! "The same value of #{gt_val} was provided for both `#{lt_par}` " \
      "and #{gt_par}. The permitted value of `#{lt_par}` must be " \
      "less than the permitted value provided for `#{gt_par}`."
    end
  end

  apply(eq_param) do |val|
    if is_unqualified?(field)
      scope.where("#{field} = ?", val)
    else
      scope.where(model.arel_table[field].eq(val))
    end
  end

  apply(gt_param) do |val|
    if is_unqualified?(field)
      scope.where("#{field} > ?", val)
    else
      scope.where(model.arel_table[field].gt(val))
    end
  end

  apply(lt_param) do |val|
    if is_unqualified?(field)
      scope.where("#{field} < ?", val)
    else
      scope.where(model.arel_table[field].lt(val))
    end
  end

  apply(gte_param) do |val|
    if is_unqualified?(field)
      scope.where("#{field} >= ?", val)
    else
      scope.where(model.arel_table[field].gte(val))
    end
  end

  apply(lte_param) do |val|
    if is_unqualified?
      scope.where("#{field} <= ?", val)
    else
      scope.where(model.arel_table[field].lte(val))
    end
  end
end
scope(&block) click to toggle source
# File lib/magiq/query.rb, line 20
def self.scope(&block)
  @scope_proc = block
end
scope_proc() click to toggle source
# File lib/magiq/query.rb, line 24
def self.scope_proc
  @scope_proc || -> { model.unscoped }
end
sort(fields) click to toggle source
# File lib/magiq/query.rb, line 122
def self.sort(fields)
  param :sort, type: :string, array: :always, limit: fields.size
  apply :sort do |raw_vals|
    vals = raw_vals.is_a?(Array) ? raw_vals : raw_vals.split(',')

    vals.reduce(scope) do |scope, val|
      col = if val.start_with?('-')
        direction = :desc
        val.sub('-', '').to_sym
      elsif val.start_with?('+')
        direction = :asc
        val.sub('+', '').to_sym
      else
        bad! "A sort order was not specified for the sort field value " \
        "'#{val}', it must be prefixed with either a plus (+#{val}) for " \
        "ascending order, or a minus (-#{val}) for decending order"
      end

      unless fields.include?(col)
        bad! "A provided sorting field: #{col}, is unknown or unsortable. The " \
        "permitted values are: #{fields.join(', ')}"
      end

      if is_unqualified?(col)
        scope.order("\"#{col}\" #{direction}")
      else
        scope.order(col => direction)
      end
    end
  end
end
toggle(*fields) click to toggle source
# File lib/magiq/query.rb, line 113
def self.toggle(*fields)
  fields.each do |field|
    param(field, type: :bool)
    apply(field) do |val|
      scope.where(field => val)
    end
  end
end
unqualified(fields) click to toggle source
# File lib/magiq/query.rb, line 32
def self.unqualified(fields)
  unqualified_columns.concat(fields)
end
unqualified_columns() click to toggle source
# File lib/magiq/query.rb, line 28
def self.unqualified_columns
  @unqualified_columns ||= []
end

Public Instance Methods

apply!() click to toggle source
# File lib/magiq/query.rb, line 372
def apply!
  each_listener_for :apply do |seek_params, opts, op|
    next update_scope! instance_exec(&op) if seek_params.empty?
    next if !opts[:any] && !seek_params.all? { |p| params.key?(p) }

    vals = seek_params.map { |p| params[p] }
    update_scope! instance_exec(*vals, &op)
  end
end
bad!(message) click to toggle source
# File lib/magiq/query.rb, line 386
def bad!(message)
  raise BadParamError, message
end
builder() click to toggle source
# File lib/magiq/query.rb, line 276
def builder
  self.class.builder
end
check!() click to toggle source
# File lib/magiq/query.rb, line 359
def check!
  @model = instance_exec(&self.class.model_proc)
  @scope = instance_exec(&self.class.scope_proc)

  each_listener_for :check do |seek_params, opts, op|
    next instance_exec(&op) if seek_params.empty?
    next if !opts[:any] && !seek_params.all? { |p| params.key?(p) }

    vals = seek_params.map { |p| params[p] }
    instance_exec(*vals, &op)
  end
end
each_listener_for(type, &block) click to toggle source
# File lib/magiq/query.rb, line 394
def each_listener_for(type, &block)
  listeners_for(type).each do |t, params, opts, op|
    block.(params, opts, op)
  end
end
extract!() click to toggle source
# File lib/magiq/query.rb, line 289
def extract!
  @params = {}

  raw_params.each_pair do |raw_key, raw_value|
    key = raw_key.to_sym

    next unless (param = builder.params[key])

    begin
      next unless (value = param.extract(raw_value))
      @params[param.key] = value
    rescue BadParamError => e
      raise BadParamError, "The `#{param.key}` parameter is invalid: " \
      "#{e.message}"
    end
  end

  @params.keys.each do |p|
    next unless (found = builder.params[p])
    next unless found.solo?

    if @params.size > 1
      raise BadParamError, "The `#{found.key}` parameter can only be used " \
      "by itself in a query."
    else
      @has_solo_param = true
      @solo_param = found
    end
  end
end
is_unqualified?(column) click to toggle source
# File lib/magiq/query.rb, line 285
def is_unqualified?(column)
  self.class.unqualified_columns.include?(column)
end
listeners_for(type) click to toggle source
# File lib/magiq/query.rb, line 390
def listeners_for(type)
  builder.listeners_for(type)
end
solo?() click to toggle source
# File lib/magiq/query.rb, line 382
def solo?
  @has_solo_param ? true : false
end
to_scope() click to toggle source
# File lib/magiq/query.rb, line 400
def to_scope
  extract!
  verify!
  check!
  apply!
  @scope
end
update_scope!(new_scope) click to toggle source
# File lib/magiq/query.rb, line 280
def update_scope!(new_scope)
  return unless new_scope
  @scope = new_scope
end
verify!() click to toggle source
# File lib/magiq/query.rb, line 320
def verify!
  if !@params
    raise RuntimeError, "verify! was called before extract!"
  end

  builder.constraints.each do |(op, keys, opts)|
    case op
    when :exclusive
      found_keys = keys.select { |k| params.key?(k) }

      next if found_keys.empty? || found_keys.one?

      raise ParamsError, "The following parameters are not permitted " \
      "to be provided together: #{found_keys.join(', ')}"
    when :mutual
      exclusives = opts[:exclusive] && Array(opts[:exclusive]) || []
      found_keys = keys.select { |k| params.key?(k) }
      found_excl = exclusives.select { |k| params.key?(k) }

      next if found_keys.empty?
      next if found_keys.empty? && found_excl.empty?
      next if found_keys == keys && found_excl.empty?

      if found_excl.any?
        raise ParamsError, "The provided " \
        "parameter#{found_keys.one? ? '' : 's'}: " \
        "#{found_keys.map { |k| "`#{k}`" }.join(', ')} " \
        "#{found_keys.one? ? 'is' : 'are'} mutually exclusive to: " \
        "#{found_excl.map { |k| "`#{k}`" }.join ', '}."
      end

      raise ParamsError, "The provided " \
      "parameter#{found_keys.one? ? '' : 's'}: " \
      "#{found_keys.map { |k| "`#{k}`" }.join(', ')} requires: " \
      "#{(keys - found_keys).map { |k| "`#{k}`" }.join(', ')}."
    end
  end
end