/rq

Functional command-line JSON processor

Primary LanguageRustGNU General Public License v3.0GPL-3.0

rq

rq is a tiny functional language with which you can manipulate JSON. Basically, it is (an insignificant subset of!) jq, written in Rust.

Usage example

NOTE: This project is in its very early stages; lot's of essential functions—and perhaps even syntax—might be missing, and overall I can't guarantee that anything actually works. Use at your own risk :)

Installation

Use cargo install. For nix users, a dev-shell is provided by the flake; one can access it with nix develop.

Usage

Call rq with an expression, and pipe some JSON into it!

$ cat test.json
[{"name": "John Doe", "age": 43, "phones": ["+44 1234567", "+44 2345678"]}]

$ cat test.json | rq '\x -> x.0.phones.1'
+44 2345678

The expression language

  • Constants: null, false, true, 1, 2.6, "string".

  • Lambdas, which can be written in various ways:

      \x -> x        |x| x        λx → x
    
  • Application is done via whitespace: (\x -> x 1 2) const. This would be akin to

      (|x| x(1, 2))(const)
    

    in pseudo-Rust notation (const is a builtin function).

  • Binary operations:

    • Arithmetic operations, with the usual precedence rules of * and / being preferred over + and -:

        1 + 3 * 5 + 4 - 7    ≡    ((1 + (3 * 5)) + 4) - 7
      
    • Comparison operations:

        1 = 3 * 5 + 4 - 7    ≡    1 = (((3 * 5) + 4) - 7)
      
        1 < 4 + 5 = 5        ≡    (1 < (4 + 5)) = 5
      

    The following table details the precedence rules:

    Op Precedence
    *, / 3
    +, - 2
    =, !=, <, <=, >, >= 1
  • If-then-else expressions:

      if 5 = 2 + 3 then "wurble" else 4  ≡  if (5 = (2 + 3)) then "wurble" else 4
                                         ≡  "wurble"
    
  • Arrays: [1, 3, null]. Arrays can contain arbitrary expressions:

      λx → [1, get 0 x, if false then 1 else 5]
    
  • Objects: { "this": 3, "that": null }. Apostrophes can be omitted:

      { this: 3, that: null }    ≡    { "this": 3, "that": null }
    

    In fact, keys can be arbitrary expressions—just make sure they actually evaluate to something sensible!

      { if true then "this" else "thus": 3, that: null }
        ≡  { "this": 3, "that": null }
    

Syntactic sugar

  • The get function—with which one can index arrays and objects—can be abbreviated by .:

      λx → x.0.this     ≡    λx → get "this" (get 0 x)
    
      (λx → x.0.this) [{this: 4}]    ≡    4
    

    Note that this syntax is only available if the to-be-indexed-thing is a variable.

      [1, 2, 3].0     # Parse error!
    
  • Instead of manually composing functions, | may be used instead;

      (get 0 | λx → { x.id: x.name }) [{id: 42, name: "Arthur"}, 4]
        ≡  { 42: Arthur }
    
  • Various binary operators can be written in pettier/alternative ways:

    • Multiplication: *, ·
    • Division: /, ÷
    • Equality: =, ==
    • Non-equality: !=, /=,
    • Less-or-equal: <=,
    • Bigger-or-equal: >=,

Standard library

  • Operators:

    (+)  : JSON  JSON  JSON
    (-)  : JSON  JSON  JSON
    (*)  : JSON  JSON  JSON
    (/)  : JSON  JSON  JSON
    (=)  : JSON  JSON  JSON
    (!=) : JSON  JSON  JSON
    (<)  : JSON  JSON  JSON
    (<=) : JSON  JSON  JSON
    (>)  : JSON  JSON  JSON
    (>=) : JSON  JSON  JSON
  • Higher order functions:

    -- `map f xs` applies `f` to every "value" in `xs`, which may be an
    -- array (in which case value means element), or an array (in which
    -- case it really means value).
    map : (JSON  JSON)  JSON  JSON
    
    -- Like map, `filter p xs` applies `p` to every value of `xs`.
    -- Keep the elements for which the predicate returns truthy.
    filter : (JSON  JSON)  JSON  JSON
  • Misc

    id    : JSON  JSON,
    
    -- Return the first argument
    const : JSON  JSON  JSON
    
    -- `get i x` gets the i'th thing out of x. I should be (evaluate to) a
    -- number or a string, with x evaluating to array or object, respectively.
    get   : JSON  JSON  JSON

REPL

A REPL is provided for getting familiar with the language; either call rq without arguments, or with a repl positional argument:

$ rq
λ>

By default, expressions will first be type-checked, and then evaluated as far as they can:

λ> 1 + 2
3

λ> |x| x
λx'. x'

λ> \x -> x x
Occurs check: can't construct infinite type: b ≡ b → c

λ> \x -> ids x
variable not in scope: ids

λ> (get 0 | λx → { x.id: x.name }) [{id: 42, name: "Arthur"}, 4]
{ 42: Arthur }

Additionally, the following keywords are available:

  • Pretty-print the expression given (this just runs the parser, followed by the pretty-printer): :e

    λ> :e \x -> x x
    λx. (x x)
    
    λ> :e \x -> get 0 x + 3 * 5 - 7
    λx. (- (+ (get 0 x) (· 3 5)) 7)
    
  • Type-check an expression, and print the type: :t

    λ> :t \f -> \g -> \x -> f x (g x)
    (a → b → c) → (a → b) → a → c
    
    λ> :t \x -> get 0 x + 3 * 5 - 7
    JSON → JSON
    
    λ> :t map
    (JSON → JSON) → JSON → JSON
    
    λ> :t \x -> x x
    Occurs check: can't construct infinite type: b ≡ b → c
    
  • Debugging: :d

    λ> :d \x -> x 4 "flurble"
    Lam("x", App(App(Var("x"), Const(Num(OrderedFloat(4.0)))), Const(String("flurble"))))
    
    • Prettier, yet more verbose, output: :dp

        λ> :dp \x -> x x
        Lam(
            "x",
            App(
                Var(
                    "x",
                ),
                Var(
                    "x",
                ),
            ),
        )