Skip to content

SpecDrivenDesign/lql-go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LQL (Logical Query Language) — User Guide

A first step towards Spec Driven Design.

1. What Is LQL?

LQL is a compact, strongly typed DSL that lets you write powerful filters and validations on JSON‑like data. By emphasizing clear syntax, explicit type handling, and an extensible library system, LQL ensures reliable, unambiguous expressions that work consistently across different environments and languages.

LQL strictly enforces typing rules—no implicit conversions—ensuring consistent behavior across different implementations.

Additionally, LQL is an experiment for Spec Driven Design, using Generative AI technologies to convert formal specification documents into useful code. 99% of the content in this repository was generated by an LLM, with the goal of pioneering a technique to split the job of software engineer into two components, the systems design aspect and the code writing aspect.


2. Example Expressions

  1. Age & Country Check:

    ($user.age >= 18 && $user.country == "US")
  2. Conditional:

    cond.ifExpr($user.isActive, "allowed", "blocked")
  3. Time Comparison:

    time.isBefore(time.parse("2025-01-01","dateOnly"), time.now())
  4. Regex:

    regex.match("^[A-Z]{3}-\\d+$", $ticket.code)
  5. String Concatenation:

    string.concat($user.firstName, " ", $user.lastName)

3. Using the LQL CLI

3.1 Installation

  1. Clone or Download the repository containing the LQL CLI.
  2. Ensure Go 1.24+ (or later) is installed on your machine.
  3. Build:
    go build -o lql
  4. Test:
    ./lql --help
    You should see usage details for the LQL command-line interface.

3.2 CLI Subcommands

The LQL CLI now supports the following subcommands:

  • compile
  • exec
  • repl
  • test
  • validate
  • highlight
  • analyze
  • export-contexts

Run lql <subcommand> --help to see extra flags for each.

lql compile

Compiles a single LQL expression into a binary bytecode file. You can supply the expression via -expr or via -in (if both are provided, the file takes precedence). Optionally, the output may be signed using an RSA private key.

lql compile [OPTIONS]

Key options:

  • -expr "<expression>": Inline expression to compile.
  • -in <filename>: Path to a file containing the expression.
  • -out <filename> (required): Output file for bytecode.
  • -signed: Indicate signing is desired.
  • -private <keyfile>: RSA private key (PKCS#1, PEM format) for signing (required if -signed).

Examples:

  1. From Inline Expression:

    lql compile -expr "$user.age >= 18 && $user.country == \"US\"" -out policy.lql

    Outputs the compiled bytecode into policy.lql.

  2. From File:

    lql compile -in rules.txt -out compiled.lql

    Reads the expression from rules.txt.

  3. Signed:

    lql compile -in rules.txt -out compiled.lqlx -signed -private private.pem

    Signs the generated bytecode with private.pem.


lql exec

Executes either a raw LQL expression or a compiled bytecode file. You can pass context data (as JSON or YAML) via stdin.

lql exec [OPTIONS]

Options:

  • -expr "<expression>": Evaluate an inline expression directly.
  • -in <filename>: Load a compiled bytecode file.
  • -signed: Indicates the bytecode is signed (only valid if -in is used).
  • -public <keyfile>: RSA public key file (PKCS#1, PEM) to verify signed bytecode.
  • -format=json|yaml: How to parse the context data from stdin (default is yaml).

Examples:

  1. Raw Expression:

    echo "{user: {age: 20, country: \"US\"}}" | lql exec -expr "$user.age >= 18 && $user.country == \"US\""

    Reads context from stdin (YAML) and evaluates the expression.

  2. Compiled Bytecode:

    echo "{user: {age: 20, country: \"US\"}}" | lql exec -in policy.lql

    Loads policy.lql for the DSL logic.

  3. Signed Bytecode:

    echo "{user: {age: 20, country: \"US\"}}" | lql exec -in compiled.lqlx -signed -public public.pem

    Verifies the signature via public.pem before execution.


lql repl

The REPL (Read-Eval-Print Loop) subcommand lets you interactively evaluate an LQL expression against different context objects. The DSL expression is provided on the command line via -expr, and context data can be supplied via stdin—either by piping a stream of JSON or YAML objects or by entering them interactively.

lql repl [OPTIONS]

Options:

  • -expr "<expression>" (Required): The DSL expression to evaluate (e.g., \$a + \$b).

Examples:

  1. Piped Input:

    echo '{"a": 5, "b": 3}' | lql repl -expr "\$a + \$b"

    Evaluates the JSON context, outputting 8.

  2. Interactive Mode:

    lql repl -expr "\$a + \$b"

    Then, at the prompt, enter:

    {"a": 10, "b": 15}

    The REPL will output:

    25
    

    An empty line exits the REPL.


lql validate

Validates a DSL expression by processing it through the lexer and parser. The expression can be provided via the -expr flag or through a file using -in (if both are provided, the file takes precedence). If the expression is valid, the command exits with code 0; otherwise, it prints the error and exits with code 1.

lql validate [OPTIONS]

Key options:

  • -expr "<expression>": Inline DSL expression to validate.
  • -in <filename>: File containing the DSL expression to validate.

Examples:

  1. Validating an Inline Expression:

    lql validate -expr "\$a + \$b"

    If the expression is valid, it exits with code 0.

  2. Validating Expression from a File:

    lql validate -in expression.lql

    Reads the expression from expression.lql, validates it, and prints the result.


lql test

Runs a series of DSL tests to quickly validate expressions against contexts. The test files contain small definitions specifying expressions, contexts, and expected outcomes (error or value). This command is mainly used for testing LQL itself.

lql test [OPTIONS]

Notable options:

  • --test-file=FILENAME (default: testcases.yml)
  • --fail-fast: Stop on the first test failure.
  • --verbose: Toggle verbose mode.
  • --output=text|yaml: Choose output format (default is text).
  • --benchmark: When enabled, each test expression is run 1,000 times and benchmark information (elapsed time and operations per second) is printed (only applicable for function call expressions).

Example:

lql test --test-file=example_tests.yml --fail-fast --output=text --benchmark

lql highlight

Parses an LQL expression to confirm validity, then prints out a colorized version (based on one of the available themes). This is useful for visually checking the expression’s structure.

lql highlight [OPTIONS]

Key options:

  • -expr "<expression>" (required): Inline LQL expression to parse and highlight.
  • -theme mild|vivid|dracula|solarized: Which color theme to use (default is mild).

Examples:

  1. Basic Highlight:

    lql highlight -expr "$user.age >= 18 && $user.country == \"US\"" -theme dracula

    Displays a colorized version using the Dracula theme.

  2. No Theme Specified:

    lql highlight -expr "(1 + 2) * 3"

    Defaults to the mild palette.


lql analyze

Parses a DSL expression, runs a static analyzer on the resulting AST, and prints any warnings or errors found. This command is useful for detecting potential semantic issues before execution.

lql analyze [OPTIONS]

Options:

  • -expr "<expression>": Inline DSL expression to analyze.
  • -in <filename>: File containing the DSL expression to analyze.

Example:

lql analyze -expr "$user.age >= 18 && $user.country == \"US\""

If issues are detected, errors are printed in red and warnings in yellow. If there are no issues, it prints "No issues found."


lql export-contexts

Extracts and prints context identifiers (e.g. variable names starting with $) from a DSL expression. This is useful when you want to know which context fields your expression depends on.

lql export-contexts [OPTIONS]

Options:

  • -expr "<expression>": Inline DSL expression to extract context identifiers from.
  • -in <filename>: File containing the DSL expression.

Example:

lql export-contexts -expr "$user.age >= 18 && $user.country == \"US\""

This command prints each context identifier (like user) on a separate line.


3.3 Generating an RSA Key Pair (PKCS#1)

If you wish to sign your compiled bytecode (-signed) or verify it in lql exec, you’ll need an RSA key pair in PKCS#1 format. Here’s how to generate it with OpenSSL:

# Generate a 2048-bit RSA private key (PKCS#1 format):
openssl genrsa -out private.pem -traditional 2048

# Extract the matching public key:
openssl rsa -in private.pem -RSAPublicKey_out -out public.pem

Use:

  • private.pem when running lql compile -signed -private private.pem
  • public.pem when running lql exec -signed -public public.pem

3.4 CLI Usage Examples

1) Testing:

lql test --test-file=example_tests.yml --fail-fast --output=text --benchmark

Runs all tests in example_tests.yml, stopping on the first failure and printing benchmark info.

2) Compiling:

lql compile -expr "math.abs(-42)" -out abs_rule.lql

Creates abs_rule.lql containing the bytecode for math.abs(-42).

3) Executing Raw Expression:

echo '{user:{name:"Alice"}}' | lql exec -expr "$user.name == 'Alice'"

Evaluates to true if the context’s $user.name is "Alice".

4) Executing Compiled Bytecode:

echo '{user:{age:17,country:"US"}}' | lql exec -in policy.lql

If policy.lql expresses $user.age >= 18 && $user.country == "US", the output is false for age=17.

5) Signed Bytecode:

echo '{order:{items:[{price:10},{price:20}]}}' | \
  lql exec -in compiled.lqlx -signed -public public.pem

Checks the signature before evaluation.

6) REPL Mode:

  • Piped Input:

    echo '{"a": 5, "b": 3}' | lql repl -expr "\$a + \$b"

    Outputs 8.

  • Interactive:

    lql repl -expr "\$a + \$b"

    At the prompt, enter:

    {"a": 10, "b": 15}

    The REPL will output:

    25
    

    (An empty line exits the REPL.)

7) Validating an Expression:

  • Inline Expression:

    lql validate -expr "\$a + \$b"

    Exits with code 0 if valid.

  • From a File:

    lql validate -in expression.lql

    Reads, validates, and prints the expression result.

8) Analyzing an Expression:

lql analyze -expr "$user.age >= 18 && $user.country == \"US\""

Runs static analysis on the expression and prints warnings or errors if found.

9) Exporting Context Identifiers:

lql export-contexts -expr "$user.age >= 18 && $user.country == \"US\""

Prints out each context identifier (e.g. user).


4. Core LQL Language Features

Below is a quick overview of the LQL language itself—its syntax, data types, and operators. If you are just using lql exec, you can embed these expressions in files or inline strings.

4.1 Basic Syntax

  • Context References: Must start with $, e.g., $user, $order.items[0].
  • Operators:
    • Arithmetic: +, -, *, /
    • Relational: <, <=, >, >=
    • Equality: ==, !=
    • Logical: AND, OR, NOT (or &&, ||, !)
  • Literals:
    • Numbers (123, 2.5e3), strings ("hi", 'hello'), booleans (true, false), null.
  • Comments: Lines starting with #.

4.2 Data Types

  1. int (64-bit)

    • Examples: 42, -100.
    • No automatic float conversion.
  2. float (64-bit)

    • Examples: 3.14, 1e10.
    • No automatic int conversion.
  3. string

    • Enclosed in single or double quotes, with escape sequences.
  4. boolean

    • Lowercase true or false only.
  5. null

    • Explicitly the literal null.
  6. object

    • JSON-like: { key: <expr>, "anotherKey": <expr> }.
    • No duplicate keys, no trailing commas.
  7. array

    • Square-bracketed: [expr1, expr2, ...].
    • No trailing commas or empty slots.
  8. time

    • Must be created/manipulated via the time library (e.g., time.parse(...)).

4.3 Operators

Operator Description Example
+, -, *, / Arithmetic (numbers only) ($a + $b) / 2
==, != Equality/inequality (num, str, bool) $value != null
<, <=, >, >= Relational (num or string) $str < "Smith"
AND, OR, NOT or &&, ` , !`
unary - Negation (numbers only) -($score + 5)

4.4 Optional Chaining

$order?.items?[0]?.price
  • If $order or .items is missing or not an array, yields null instead of erroring.
  • If [0] is out of range, yields null.

4.5 Inline Literals (Arrays and Objects)

  • Array: [1, 2, (3+4)][1, 2, 7].
  • Object: { name: "Alice", "home-city": "NYC" }.

5. Standard Libraries

Below is an expanded Section 5 (originally Section 4 in older docs) that details every function from the DSL specification—complete with signatures, return types, error conditions, and example usages. You can replace your existing library reference with this updated version.

5.1 Time Library

Time values in LQL are opaque objects that store date/time internally as a 64‑bit integer (epochMillis since 1970‑01‑01T00:00:00Z).
Important: You cannot apply <, >, etc. directly to time values. Instead, use the time library functions.

5.1.1 time.now()

  • Signature:
    time.now()
  • Return Type: Time
  • Arguments: none
  • Errors:
    • Generally none at the DSL level; system clock issues could trigger an implementation-specific error.
  • Example:
    time.now()  # returns a Time object for the current system time

5.1.2 time.parse(inputString, format[, formatDetails])

  • Signature:
    time.parse(string, string [, string])
  • Return Type: Time
  • Arguments:
    1. inputString: The date/time string (e.g., "2025-01-01T12:00:00Z").
    2. format: Format identifier—"iso8601", "dateOnly", "epochMillis", "rfc2822", or "custom".
    3. formatDetails (optional): Required if format == "custom" (e.g., "yyyy-MM-dd HH:mm:ss").
  • Errors:
    • Runtime/Semantic Error if inputString does not match the format or if an unknown format is specified.
  • Examples:
    # Parse ISO8601
    time.parse("2025-01-01T12:00:00Z", "iso8601")
    
    # Parse a date-only
    time.parse("2025-01-01", "dateOnly")
    
    # Parse epochMillis
    time.parse("1735732800000", "epochMillis")
    
    # Parse with a custom format
    time.parse("2025-02-13 10:00:00", "custom", "yyyy-MM-dd HH:mm:ss")

5.1.3 time.add(timeVal, durationMillis)

  • Signature:
    time.add(Time, numeric)
  • Return Type: Time
  • Arguments:
    1. timeVal: A valid Time object.
    2. durationMillis: A numeric value (int or float) representing milliseconds to add.
  • Errors:
    • Runtime Error if timeVal is not Time or durationMillis is not numeric.
  • Example:
    # Adds 24h (86400000 ms) to the given date
    time.add(time.parse("2025-01-01", "dateOnly"), 86400000)

5.1.4 time.subtract(timeVal, durationMillis)

  • Signature:
    time.subtract(Time, numeric)
  • Return Type: Time
  • Arguments: Same as time.add, except the duration is subtracted.
  • Errors:
    • Runtime Error if timeVal is not Time or durationMillis is not numeric.
  • Example:
    time.subtract(time.parse("2025-01-02", "dateOnly"), 86400000)
    # => Time corresponding to 2025-01-01

5.1.5 time.diff(timeVal1, timeVal2)

  • Signature:
    time.diff(Time, Time)
  • Return Type: numeric (milliseconds)
  • Arguments: Two valid Time objects.
  • Errors:
    • Runtime Error if either argument is not Time.
  • Example:
    time.diff(
      time.parse("2025-02-13", "dateOnly"),
      time.parse("2025-02-01", "dateOnly")
    )
    # => 12 days * 86400000 = 1036800000

5.1.6 time.isBefore(timeValA, timeValB)

  • Signature:
    time.isBefore(Time, Time)
  • Return Type: boolean
  • Errors:
    • Runtime Error if either argument is not Time.
  • Example:
    time.isBefore(
      time.parse("2025-01-01T00:00:00Z", "iso8601"),
      time.now()
    )

5.1.7 time.isAfter(timeValA, timeValB)

  • Signature:
    time.isAfter(Time, Time)
  • Return Type: boolean
  • Errors: Same as time.isBefore.
  • Example:
    time.isAfter(time.parse("2025-12-31T23:59:59Z","iso8601"), time.now())

5.1.8 time.isEqual(timeValA, timeValB)

  • Signature:
    time.isEqual(Time, Time)
  • Return Type: boolean
  • Errors: Same as time.isBefore.
  • Example:
    time.isEqual(
      time.parse("2025-01-01T00:00:00Z","iso8601"),
      time.parse("2025-01-01T00:00:00Z","iso8601")
    )
    # => true

5.1.9 time.format(timeVal, formatString)

  • Signature:
    time.format(Time, string)
  • Return Type: string
  • Arguments:
    1. timeVal: A valid Time object.
    2. formatString: A pattern specifying how to format the date/time (e.g. "yyyy-MM-dd HH:mm").
  • Errors:
    • Runtime Error if timeVal is not Time or formatString is invalid.
  • Example:
    time.format(
      time.parse("2025-01-01T12:00:00Z","iso8601"),
      "yyyy/MM/dd HH:mm"
    )
    # => "2025/01/01 12:00"

5.1.10 time.toEpochMillis(timeVal)

  • Signature:
    time.toEpochMillis(Time)
  • Return Type: numeric (64-bit integer)
  • Errors:
    • Runtime Error if timeVal is not a Time object.
  • Example:
    time.toEpochMillis(time.parse("2025-01-01", "dateOnly"))
    # => 1735689600000 (depending on the time zone used)

5.1.11 time.getYear(timeVal), time.getMonth(timeVal), time.getDay(timeVal), etc.

These functions extract components from a Time object. Implementations may add more (like time.getHour, time.getMinute, etc.).

  • Signature:
    time.getYear(Time) -> numeric
    time.getMonth(Time) -> numeric
    time.getDay(Time) -> numeric
  • Errors:
    • Runtime Error if argument is not Time.
  • Example:
    time.getYear(time.parse("2025-01-01", "dateOnly"))
    # => 2025

5.1.12 time.startOfDay(timeVal) / time.endOfDay(timeVal)

  • Signature:
    time.startOfDay(Time) -> Time
    time.endOfDay(Time) -> Time
  • Errors:
    • Runtime Error if argument is not Time.
  • Example:
    time.startOfDay(time.parse("2025-01-01T12:34:56Z","iso8601"))
    # => Time object for 2025-01-01T00:00:00Z

5.1.13 time.withZone(timeVal, zoneName)

  • Signature:
    time.withZone(Time, string) -> Time
  • Errors:
    • Runtime Error if timeVal is not Time or zoneName is invalid.
  • Behavior:
    Returns a new Time object representing the same instant in the specified time zone. (Internally, epochMillis usually doesn’t change; formatting does.)
  • Example:
    time.withZone(time.parse("2025-01-01T12:00:00Z","iso8601"), "America/New_York")

5.2 Math Library

All math functions operate on numeric (int or float) arguments. Using them on non-numeric types raises a Runtime Error.

5.2.1 math.abs(x)

  • Signature:
    math.abs(numeric)
  • Return Type: numeric
  • Example:
    math.abs(-42)  # => 42

5.2.2 math.floor(x)

  • Signature:
    math.floor(numeric)
  • Return Type: numeric
  • Behavior: Rounds down to the nearest integer.
  • Example:
    math.floor(3.9)  # => 3

5.2.3 math.ceil(x)

  • Signature:
    math.ceil(numeric)
  • Return Type: numeric
  • Behavior: Rounds up to the nearest integer.
  • Example:
    math.ceil(3.1)   # => 4

5.2.4 math.round(x)

  • Signature:
    math.round(numeric)
  • Return Type: numeric
  • Behavior: Rounds to the nearest integer (details of half-up vs. half-to-even are implementation-specific).
  • Example:
    math.round(3.5)  # => 4   (assuming half-up)

5.2.5 math.sqrt(x)

  • Signature:
    math.sqrt(numeric)
  • Return Type: numeric
  • Errors:
    • Runtime Error if x < 0 (unless imaginary is supported).
  • Example:
    math.sqrt(16)   # => 4

5.2.6 math.pow(x, y)

  • Signature:
    math.pow(numeric, numeric)
  • Return Type: numeric
  • Example:
    math.pow(2, 3)  # => 8

5.2.7 math.sum(arr[, subfield, defaultVal])

  • Signature:
    math.sum(array [, string, numeric])
  • Return Type: numeric
  • Behavior:
    • If subfield is given, each element of arr is assumed to be an object, and the function sums element[subfield].
    • If defaultVal is given, that is used when a subfield is missing or if the array is empty.
  • Errors:
    • Runtime Error if arr is not an array or if elements are not numeric (and no defaultVal).
  • Example:
    # Summation of array elements
    math.sum([1, 2, 3])  # => 6
    
    # Summation of subfield "price" in an array of objects
    math.sum($order.items, "price", 0)

5.2.8 math.min(arr[, subfield, defaultVal])

  • Signature:
    math.min(array [, string, numeric])
  • Return Type: numeric
  • Behavior: Similar to math.sum, but returns the minimum value.
  • Example:
    math.min([5,2,7])  # => 2

5.2.9 math.max(arr[, subfield, defaultVal])

  • Signature:
    math.max(array [, string, numeric])
  • Return Type: numeric
  • Behavior: Similar to math.min, but returns the maximum value.
  • Example:
    math.max([5,2,7])  # => 7

5.2.10 math.avg(arr[, subfield, defaultVal])

  • Signature:
    math.avg(array [, string, numeric])
  • Return Type: numeric (float or int, depending on implementation)
  • Behavior: Computes average of numeric elements (or subfield values).
  • Example:
    math.avg([5,5,5])  # => 5

5.3 String Library

All string functions require string arguments unless otherwise specified. Non-string arguments produce Runtime Errors.

5.3.1 string.toLower(s)

  • Signature:
    string.toLower(string)
  • Return Type: string
  • Example:
    string.toLower("HELLO")  # => "hello"

5.3.2 string.toUpper(s)

  • Signature:
    string.toUpper(string)
  • Return Type: string
  • Example:
    string.toUpper("Hello")  # => "HELLO"

5.3.3 string.trim(s)

  • Signature:
    string.trim(string)
  • Return Type: string
  • Behavior: Removes leading and trailing whitespace.
  • Example:
    string.trim("  hello  ")  # => "hello"

5.3.4 string.startsWith(s, prefix)

  • Signature:
    string.startsWith(string, string) -> boolean
  • Example:
    string.startsWith("Hello World", "Hell")  # => true

5.3.5 string.endsWith(s, suffix)

  • Signature:
    string.endsWith(string, string) -> boolean
  • Example:
    string.endsWith("Hello World", "World")  # => true

5.3.6 string.contains(s, substring)

  • Signature:
    string.contains(string, string) -> boolean
  • Example:
    string.contains("Hello World", "lo Wo")  # => true

5.3.7 string.replace(s, old, new[, limit])

  • Signature:
    string.replace(string, string, string [, numeric]) -> string
  • Behavior: Replaces all occurrences of old with new by default. If limit is provided, replace up to that many occurrences.
  • Example:
    string.replace("abc-123-xyz", "-", ":")  # => "abc:123:xyz"

5.3.8 string.split(s, delim)

  • Signature:
    string.split(string, string) -> array of strings
  • Example:
    string.split("one,two,three", ",")
    # => ["one", "two", "three"]

5.3.9 string.join(arrOfStrings, sep)

  • Signature:
    string.join(array, string) -> string
  • Behavior: Joins elements of arrOfStrings (which must be an array of strings) into a single string, separated by sep.
  • Example:
    string.join(["one","two","three"], ",")
    # => "one,two,three"

5.3.10 string.substring(s, start, length)

  • Signature:
    string.substring(string, numeric, numeric) -> string
  • Behavior: Returns a substring from index start of s with the specified length.
  • Example:
    string.substring("Hello", 1, 3)
    # => "ell"

5.3.11 string.indexOf(s, sub[, fromIndex])

  • Signature:
    string.indexOf(string, string [, numeric]) -> numeric
  • Behavior: Returns the zero-based index of the first occurrence of sub in s at or after fromIndex. If not found, returns -1.
  • Example:
    string.indexOf("banana", "ana")   # => 1
    string.indexOf("banana", "ana", 2)# => 3

5.3.12 string.concat(str1, str2, ...)

  • Signature:
    string.concat(string, string, ...)
  • Return Type: string
  • Behavior: Concatenates multiple strings in order.
  • Example:
    string.concat("Hello", " ", "World")  # => "Hello World"

5.4 Regex Library

For advanced pattern matching. Unlike string.replace, these take regex patterns that may include anchors, groups, etc.

5.4.1 regex.match(pattern, s)

  • Signature:
    regex.match(string, string) -> boolean
  • Behavior: Returns true if any substring of s matches pattern, or false otherwise.
  • Example:
    regex.match("^[A-Z]{3}-\\d+$", "ABC-123")  # => true

5.4.2 regex.replace(s, pattern, replacement)

  • Signature:
    regex.replace(string, string, string) -> string
  • Behavior: Replaces all occurrences of pattern in s with replacement. Supports capturing groups in replacement (e.g. $1) depending on implementation.
  • Example:
    regex.replace("abc-123", "(\\d+)", "[$1]")
    # => "abc-[123]"

5.4.3 regex.find(pattern, s)

  • Signature:
    regex.find(string, string) -> string
  • Behavior: Returns the first substring of s that matches pattern. If none, returns "".
  • Example:
    regex.find("\\d+", "abc-123-xyz")  # => "123"

5.5 Array Library

All functions here require the first argument to be an array. Mismatched types raise Runtime Errors.

5.5.1 array.contains(arr, value)

  • Signature:
    array.contains(array, any) -> boolean
  • Example:
    array.contains([100, 200, 300], 200)
    # => true

5.5.2 array.find(arr, subfield, matchVal[, defaultObj])

  • Signature:
    array.find(array, string, any [, anyObject])
  • Return Type: object (or defaultObj if not found)
  • Behavior: Searches arr (an array of objects) for an element whose subfield equals matchVal.
    • If found, returns the first matching object.
    • If not found, returns defaultObj if provided; otherwise raises a runtime error.
  • Example:
    array.find($users, "id", 42, {name: "Unknown"})
    # Returns the object in $users whose .id == 42, or {name:"Unknown"}

5.5.3 array.first(arr[, defaultVal]) / array.last(arr[, defaultVal])

  • Signatures:
    array.first(array [, any]) -> any
    array.last(array [, any]) -> any
  • Behavior:
    • Returns the first (or last) element of arr.
    • If arr is empty, returns defaultVal if provided, else raises an error.
  • Example:
    array.first([1,2,3])  # => 1
    array.last([1,2,3])   # => 3

5.5.4 array.extract(arr, subfield[, defaultVal])

  • Signature:
    array.extract(array, string [, any]) -> array
  • Behavior:
    • Maps each element of arr (an array of objects) to that element’s subfield.
    • If an element lacks subfield, uses defaultVal if provided, else raises an error.
  • Example:
    # Suppose $order.items = [ {price:10}, {price:20}, {price:15} ]
    array.extract($order.items, "price")
    # => [10, 20, 15]

5.5.5 array.sort(arr[, ascending])

  • Signature:
    array.sort(array [, boolean]) -> array
  • Behavior:
    • Sorts arr in ascending order by default, or descending if ascending == false.
    • Elements must be comparable (all numeric or all strings).
  • Example:
    array.sort([3,1,2])       # => [1,2,3]
    array.sort([3,1,2], false)# => [3,2,1]

5.5.6 array.flatten(arr)

  • Signature:
    array.flatten(array) -> array
  • Behavior:
    • Flattens one level of sub-arrays into a single array.
  • Example:
    array.flatten([1, [2,3], [4], 5])
    # => [1,2,3,4,5]

5.5.7 array.filter(collection[, subfield[, matchVal]])

  • Signature:

    array.filter(array [, string [, any]])
  • Return Type: array

  • Potential Errors:

    • Runtime Error if collection is not an array.
    • Runtime Error if subfield is provided but not a string.
  • Behavior:

    • No subfield argument: Returns a new array containing all elements of collection that are not null.
    • subfield provided, but no matchVal: Returns a new array of elements whose subfield is present and not null.
    • subfield and matchVal both provided: Returns a new array of elements whose subfield strictly equals matchVal.

5.6 Conditional Library (cond)

These functions help with conditional logic or presence checks.

5.6.1 cond.ifExpr(condition, thenVal, elseVal)

  • Signature:
    cond.ifExpr(boolean, any, any) -> any
  • Behavior:
    • If condition is true, returns thenVal; else returns elseVal.
  • Example:
    cond.ifExpr($user.age >= 18, "allowed", "blocked")

5.6.2 cond.coalesce(expr1, expr2, ...)

  • Signature:
    cond.coalesce(any, any, ...) -> the first non‑null
  • Behavior:
    • Returns the first non‑null among expr1, expr2, ....
    • If all are null, raises a runtime error.
  • Example:
    cond.coalesce($user.middleName, "N/A")
    # returns "N/A" if $user.middleName is null or missing

5.6.3 cond.isFieldPresent(object, fieldPath)

  • Signature:
    cond.isFieldPresent(object, string) -> boolean
  • Behavior:
    • Returns true if object has the specified fieldPath (direct key) present (even if it’s null), else false.
  • Example:
    cond.isFieldPresent($user, "country")

5.7 Type Library

Used for type checks (predicates) and explicit conversions (no implicit conversions occur in LQL).

5.7.1 Predicates

  • type.isNumber(x)
  • type.isString(x)
  • type.isBoolean(x)
  • type.isArray(x)
  • type.isObject(x)
  • type.isNull(x)

Each returns true or false depending on whether x is of that type.

Example:

type.isNumber($val)  # => true or false

5.7.2 Conversions

  • type.string(x)

    • Converts x to a string.
    • If x is null, returns "".
    • If x is numeric, returns its decimal string representation.
    • If x is a Time, raises an error unless your implementation chooses to define a special string representation.
    • Example:
      type.string(123)   # => "123"
      type.string(null)  # => ""
  • type.int(x)

    • Converts x to a 64‑bit integer.
    • If x is null, returns 0.
    • If x is a float, it truncates (implementation-specific).
    • If x is a string, it must be a valid integer literal, else error.
    • Example:
      type.int("42")   # => 42
      type.int(null)   # => 0
  • type.float(x)

    • Converts x to a 64‑bit float.
    • If x is null, returns 0.0.
    • If x is an int, it becomes float.
    • If x is a string, it must parse correctly, else error.
    • Example:
      type.float(3)       # => 3.0
      type.float("3.14")  # => 3.14
  • type.boolean(x)

    • Converts x to a boolean.
    • If x is null, an empty string (""), the string "0", the number 0, or the boolean false, returns false.
    • If x is the string "1", the number 1, or the boolean true, returns true.
    • For any other value, returns false and raises an error indicating that the value cannot be converted to a boolean.
    • Example:
      type.boolean("1")    # => true
      type.boolean(1)      # => true
      type.boolean(true)   # => true
      type.boolean("0")    # => false
      type.boolean("")     # => false
      type.boolean(0)      # => false
      type.boolean("yes")  # => error: "type.bool: 'yes' cannot be converted to bool"

6. Error Handling

LQL reports errors in four main categories:

  1. Lexical Errors (e.g., unclosed string, malformed numeric).
  2. Syntax Errors (e.g., misplaced operators, mismatched parentheses).
  3. Semantic Errors (e.g., + on non‑numeric, < on booleans).
  4. Runtime Errors (e.g., missing fields without optional chaining, out-of-bounds array indexes).

Examples:

LexicalError: Unclosed string literal at line 1, column 16
SyntaxError: Bare identifier 'True' is not allowed at line 1, column 16
SemanticError: '+' operator used on non-numeric type at line 1, column 5
RuntimeError: field 'user' not found at line 1, column 1

7. Type Safety Best Practices

While LQL allows you to reference context fields directly (e.g. $user.age), doing so is unsafe because the field’s type is not explicitly verified. This may lead to unpredictable behavior or runtime errors if the actual data type does not match what your expression expects. It is strongly recommended to wrap context fields using explicit conversion functions to enforce type safety.


Why Use Explicit Conversions?

  1. Avoid Ambiguity:
    A bare context reference (e.g. $user.age) has an “unknown” type at analysis time. Without an explicit conversion, it’s unclear whether the value is numeric, string, or another type. This ambiguity increases the risk of type mismatches in operations such as arithmetic, comparisons, and function calls.

  2. Prevent Runtime Errors:
    Operations like arithmetic or relational comparisons require operands of a specific type. If a context field is used without conversion and its type is not what is expected (for example, a string value in an arithmetic operation), the DSL may produce errors at runtime.

  3. Improve Code Clarity:
    Explicitly wrapping context fields documents your intention. It makes it clear what type you expect, easing maintenance and reducing bugs when the data source changes.


How to Wrap Context Fields

When your DSL expression uses a context field, wrap it with the appropriate type conversion function:

  • Numeric Values:
    Use type.int() for integers or type.float() for floating‑point numbers.
    Example:

    type.int($user.age) >= 18

    Instead of:

    $user.age >= 18

    Explanation: This explicitly converts $user.age to a 64‑bit integer. If the field is not a valid integer, a clear error is raised.

  • String Values:
    Use type.string() to ensure a context field is treated as a string.
    Example:

    string.concat("Hello, ", type.string($user.firstName))

    Instead of:

    string.concat("Hello, ", $user.firstName)

    Explanation: This guarantees that the first name is converted to a string before concatenation.

  • Boolean Values:
    Use type.boolean() when a context field is expected to be boolean.
    Example:

    cond.ifExpr(type.boolean($user.isActive), "allowed", "blocked")

    Instead of:

    cond.ifExpr($user.isActive, "allowed", "blocked")

    Explanation: This explicitly converts $user.isActive to a boolean, ensuring correct evaluation.

  • Arrays and Objects:
    While less common, you may also need to wrap context fields when they are expected to be arrays or objects using type.intArray(), type.stringArray(), etc., to enforce uniformity within collection elements.


Detailed Examples

  1. Arithmetic Comparison Without Conversion (Unsafe):

    $user.age >= 18

    Risk: If $user.age is not numeric (for example, if it’s a string or missing), the expression may produce a type mismatch or runtime error.

  2. Arithmetic Comparison With Conversion (Safe):

    type.int($user.age) >= 18

    Benefit: This ensures that $user.age is explicitly converted to an integer. If the conversion fails (e.g. the value is not a valid number), a meaningful error is raised.

  3. Function Call on a Context Field Without Conversion (Unsafe):

    math.abs($user.balance)

    Risk: If $user.balance is not numeric, math.abs may fail or produce an incorrect result.

  4. Function Call With Conversion (Safe):

    math.abs(type.float($user.balance))

    Benefit: This explicitly converts $user.balance to a float, ensuring that math.abs operates on a proper numeric value.

  5. Logical Expression Using a Context Field (Unsafe):

    cond.ifExpr($user.isActive, "enabled", "disabled")

    Risk: Without conversion, the type of $user.isActive is ambiguous. An unexpected type (e.g. string "true") could lead to an error.

  6. Logical Expression With Conversion (Safe):

    cond.ifExpr(type.boolean($user.isActive), "enabled", "disabled")

    Benefit: This ensures that $user.isActive is interpreted as a boolean, avoiding potential type conflicts.

8. WebAssembly Support

LQL is not only available as a CLI tool but also as a WebAssembly (WASM) module that can be run directly in the browser. This version allows you to experiment with the LQL DSL through a convenient web interface. The WASM version provides functionality for all major commands, including:

  • Compile: Generate bytecode from an LQL expression.
  • Exec: Execute an expression or a compiled bytecode with provided context.
  • Validate: Check the correctness of an LQL expression.
  • Highlight: Get a colorized version of the expression for easier reading.
  • Export Contexts: Extract context identifiers from an expression.
  • Create, Eval, and Delete AST: Create, evaluate and delete abstract syntax trees in memory for re-use.

8.1 Overview

The WASM version is delivered as a compiled WebAssembly module (lql.wasm), accompanied by a JavaScript shim (wasm_exec.js) provided by Go. A sample HTML interface (see below) demonstrates how the WASM module can be integrated into a web page to offer interactive testing of LQL commands.

8.2 Loading and Running the WASM Module

The WASM module is loaded using the Go-provided wasm_exec.js. Once loaded, the Go runtime initializes the LQL functions and exposes them as JavaScript commands (such as compileCmd, execCmd, etc.) which are then used by the HTML interface.

For now, you must download a wasm release from https://github.com/SpecDrivenDesign/lql-go/releases and include it manually in your project. No CDN versions are available at this moment.

Example snippet from index.html:

<script src="wasm_exec.js"></script>
<script>
    // Create a new Go instance.
    const go = new Go();

    // Polyfill for browsers that don't support instantiateStreaming.
    if (!WebAssembly.instantiateStreaming) {
        WebAssembly.instantiateStreaming = async (resp, importObject) => {
            const source = await (await resp).arrayBuffer();
            return await WebAssembly.instantiate(source, importObject);
        };
    }

    // Load the WebAssembly module.
    WebAssembly.instantiateStreaming(fetch("lql.wasm"), go.importObject)
        .then((result) => {
            go.run(result.instance);
            document.getElementById("status").textContent = "WebAssembly Loaded.";
            document.getElementById("interface").style.display = "block";
        })
        .catch((err) => {
            document.getElementById("status").textContent = "Failed to load WebAssembly: " + err;
        });
</script>

About

WIP / Time library is unstable

Resources

Stars

Watchers

Forks

Packages

No packages published