No release in over 3 years
Low commit activity in last 3 years
Parse Cappuccino Literal predicated and sort descriptors to arel expressions
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

>= 0
>= 0
 Project Readme

PredicateLiteralKit4Ruby

Parses a JSON-friendly array literal representation of NSPredicate-style predicates and NSSortDescriptor-style sort descriptors and converts them into Arel nodes usable in a query. It is the server-side counterpart to the Objective-J/Cappuccino PredicateLiteralKit, which produces the literals a browser client sends to a Ruby back end.

Status: last released and last committed in December 2013 (v0.0.2), unmaintained since. It targets the standalone arel gem (Arel was folded into ActiveRecord in Rails 5.2) and ships no tests.

Installation

Published to RubyGems as version 0.0.2 (2013-12-12):

$ gem install PredicateLiteralKit4Ruby

Or in a Gemfile:

gem 'PredicateLiteralKit4Ruby'

The gemspec depends on arel, json and msgpack without version constraints. Only arel is used by the library itself; json and msgpack are used by the bundled bin/sample.rb script.

Literal format

PredicateLiteral.parse takes a plain Ruby array (typically the result of JSON.parse) and dispatches on its first element:

Literal Class Notes
["cmd", op, sub…] CompoundPredicate AND / OR / NOT over subpredicates
["cmp", op, lhs, rhs, options, modifier] ComparisonPredicate options and modifier are stored but never used
["key", "keypath"] KeypathExpression resolved against the Arel table
["con", value] ConstantExpression value passed through unchanged
["agr", expr…] AggregateExpression array of expressions, e.g. the right side of IN
["fn", "selector:", arg…] FunctionExpression Cocoa-style function selectors
["var", "NAME"] VariableExpression resolved via Object.const_get
[true] / [false] BooleanExpression builds to the integers 1 / 0, for SQLite compatibility
["slf"] SelfExpression raises — SELF has no Arel equivalent

Compound operators: "&" (AND), "|" (OR), "!" (NOT).

Comparison operators: ==, !=, <, <=, >, >=, BEGINSWITH, CONTAINS, ENDSWITH, LIKE, IN. MATCHES raises (no regex support in Arel).

Function selectors: count:, add:to:, from:substract:, multiply:by:, divide:by:, raise:to:. first:, last: and fromObject:index: are recognised but raise; anything else raises Unknown function.

Usage

Parsing returns a predicate/expression object; calling build_arel(arel_table) on it returns an Arel node.

require 'arel'
require 'PredicateLiteralKit4Ruby'

table = Arel::Table.new(:user)

literal   = ["cmp", "==", ["key", "lastName"], ["con", "Schneider"]]
predicate = PredicateLiteral.parse(literal)   # => ComparisonPredicate
predicate.build_arel(table).to_sql
# => "user"."lastName" = 'Schneider'

Rendering with to_sql requires a configured Arel engine; inside a Rails/ActiveRecord application that is already the case and the node can be handed straight to where:

User.where(PredicateLiteral.parse(literal).build_arel(User.arel_table))

More literals and the SQL they produce:

["cmd", "&", ["cmp", "==", ["key", "lastName"], ["con", "Schneider"]],
             ["cmp", ">",  ["key", "age"],      ["con", 21]]]
# "user"."lastName" = 'Schneider' AND "user"."age" > 21

["cmd", "|", ["cmp", "==", ["key", "a"], ["con", 1]],
             ["cmp", "==", ["key", "b"], ["con", 2]]]
# ("user"."a" = 1 OR "user"."b" = 2)

["cmd", "!", ["cmp", "==", ["key", "a"], ["con", 1]]]
# NOT ("user"."a" = 1)

["cmp", "LIKE", ["key", "lastName"], ["con", "Schn*"]]
# "user"."lastName" LIKE 'Schn%'

["cmp", "IN", ["key", "age"], ["agr", ["con", 1], ["con", 2], ["con", 3]]]
# "user"."age" IN (1, 2, 3)

["cmp", "==", ["fn", "add:to:", ["key", "a"], ["con", 1]], ["con", 5]]
# ("user"."a" + 1) = 5

["cmp", "==", ["fn", "count:", ["key", "a"]], ["con", 5]]
# COUNT("user"."a") = 5

["cmd", "&"]
# 1        (an empty compound predicate builds to TRUE)

Sort descriptors

SortDescriptorLiteral.parse takes an array of [key, selector, ascending] triples and returns an array of SortDescriptorLiteral::SortDescriptor objects. The selector is stored but not used.

descriptors = SortDescriptorLiteral.parse([["lastName",  "compare:", true],
                                           ["firstName", "compare:", false]])
descriptors.map { |d| d.build_arel(table).to_sql }
# => ["\"user\".\"last_name\" ASC", "\"user\".\"first_name\" DESC"]

Sort keys are looked up in the global LITERAL_KEY_MAPPING hash and, when absent, converted from camelCase to snake_case:

LITERAL_KEY_MAPPING["lastName"] = "surname"
SortDescriptorLiteral.parse([["lastName", "compare:", true]]).first.build_arel(table).to_sql
# => "user"."surname" ASC

Predicate keypaths get neither treatment — ["key", "lastName"] is used verbatim as the column name.

Known issues

  • BEGINSWITH and ENDSWITH are swapped: BEGINSWITH builds LIKE '%value' and ENDSWITH builds LIKE 'value%'. Both also interpolate the Arel node rather than the string value.
  • A "!" compound predicate negates only its first subpredicate.
  • Dotted keypaths (["key", "address.city"]) are split on . and indexed repeatedly into the Arel table, which fails for anything beyond a single segment.
  • SortDescriptor#build_arel prints KEY: … to stdout on every call.
  • ["var", "NAME"] resolves to a Ruby constant via Object.const_get, so variable names in incoming literals reach constant lookup directly.
  • bin/sample.rb is picked up as a gem executable, so installing the gem puts a sample.rb command on the PATH.

License

MIT. LICENSE.txt (Copyright 2013 Udo Schneider) and the gemspec license field agree.