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" ASCPredicate keypaths get neither treatment — ["key", "lastName"] is used verbatim as the column name.
Known issues
-
BEGINSWITHandENDSWITHare swapped:BEGINSWITHbuildsLIKE '%value'andENDSWITHbuildsLIKE '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_arelprintsKEY: …to stdout on every call. -
["var", "NAME"]resolves to a Ruby constant viaObject.const_get, so variable names in incoming literals reach constant lookup directly. -
bin/sample.rbis picked up as a gem executable, so installing the gem puts asample.rbcommand on the PATH.
License
MIT. LICENSE.txt (Copyright 2013 Udo Schneider) and the gemspec license field agree.