diff --git a/.gitattributes b/.gitattributes index d98092b..7f86c55 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,6 +4,12 @@ *.rei text eol=lf *.res text eol=lf *.resi text eol=lf +*.opam text eol=lf +*.t text eol=lf +*.sh text eol=lf +Makefile text eol=lf +dune text eol=lf +dune-project text eol=lf # Recognize all ml files as OCaml *.ml linguist-language=OCaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df5ee3d..0f0a6e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: - - '**' + - 'main' tags: - '*' pull_request: @@ -20,6 +20,7 @@ jobs: name: Build and test runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: include: - os: ubuntu-latest @@ -32,29 +33,102 @@ jobs: arch: x64 steps: + - name: "Windows: Set git config" + if: runner.os == 'Windows' + run: | + git config --system core.autocrlf false + git config --system core.eol lf + git config --system core.longpaths true + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Use Node.js - if: ${{ matrix.os == 'ubuntu-latest' }} uses: actions/setup-node@v4 with: node-version: 22.x + cache: 'npm' + + - name: Load opam cache + id: opam-cache + uses: actions/cache/restore@v5 + with: + path: | + ${{ runner.tool_cache }}/opam + ~/.opam + _opam + .opam-path + C:\cygwin + C:\.opam + key: opam-${{ matrix.os }}-${{ matrix.arch }}-${{ env.OCAML_COMPILER }}-${{ hashFiles('dune-project', 'Makefile') }} - name: Use OCaml ${{ env.OCAML_COMPILER }} uses: ocaml/setup-ocaml@v3 + if: steps.opam-cache.outputs.cache-hit != 'true' with: ocaml-compiler: ${{ env.OCAML_COMPILER }} dune-cache: true opam-disable-sandboxing: true + - name: Get OPAM executable path + if: steps.opam-cache.outputs.cache-hit != 'true' + uses: actions/github-script@v7 + with: + script: | + const opam_path = await io.which('opam', true); + console.log('opam executable found: %s', opam_path); + const Fs = require('fs/promises'); + await Fs.writeFile('.opam-path', opam_path, 'utf-8'); + console.log('stored path to .opam-path'); + + - name: Use cached OPAM environment + if: steps.opam-cache.outputs.cache-hit == 'true' + run: | + # https://github.com/ocaml/setup-ocaml/blob/v3.4.6/packages/setup-ocaml/src/installer.ts + echo "OPAMCOLOR=always" >> "$GITHUB_ENV" + echo "OPAMCONFIRMLEVEL=unsafe-yes" >> "$GITHUB_ENV" + echo "OPAMDOWNLOADJOBS=4" >> "$GITHUB_ENV" + echo "OPAMERRLOGLEN=0" >> "$GITHUB_ENV" + echo "OPAMEXTERNALSOLVER=builtin-0install" >> "$GITHUB_ENV" + echo "OPAMPRECISETRACKING=1" >> "$GITHUB_ENV" + echo "OPAMRETRIES=10" >> "$GITHUB_ENV" + echo "OPAMSOLVERTIMEOUT=600" >> "$GITHUB_ENV" + echo "OPAMYES=1" >> "$GITHUB_ENV" + echo "CLICOLOR_FORCE=1" >> "$GITHUB_ENV" + + if [[ "$RUNNER_OS" != "Windows" ]]; then + echo "OPAMROOT=$HOME/.opam" >> "$GITHUB_ENV" + else + echo "OPAMROOT=C:\\.opam" >> "$GITHUB_ENV" + fi + + OPAM_PATH="$(cat .opam-path)" + chmod +x "$OPAM_PATH" + dirname "$OPAM_PATH" >> "$GITHUB_PATH" + + if [[ "$RUNNER_OS" == "Windows" ]]; then + fsutil behavior query SymlinkEvaluation + fsutil behavior set symlinkEvaluation R2L:1 R2R:1 + fsutil behavior query SymlinkEvaluation + + echo "HOME=$USERPROFILE" >> "$GITHUB_ENV" + echo "MSYS=winsymlinks:native" >> "$GITHUB_ENV" + echo "CYGWIN=winsymlinks:native" >> "$GITHUB_ENV" + echo "BASH_ENV=C:\\cygwin\\bash_env" >> "$GITHUB_ENV" + + # Add Cygwin bin to PATH - required for OCaml binaries to find DLLs + echo "C:\\cygwin\\bin" >> "$GITHUB_PATH" + echo "C:\\cygwin\\wrapperbin" >> "$GITHUB_PATH" + fi + shell: bash + - name: Pin some deps - if: matrix.os != 'windows-latest' + if: steps.opam-cache.outputs.cache-hit != 'true' && matrix.os != 'windows-latest' run: make pin - name: Pin some deps (Windows with retry) - if: matrix.os == 'windows-latest' + if: steps.opam-cache.outputs.cache-hit != 'true' && matrix.os == 'windows-latest' shell: pwsh run: | $maxRetries = 3 @@ -70,11 +144,11 @@ jobs: if ($LASTEXITCODE -ne 0) { exit 1 } - name: Install deps - if: matrix.os != 'windows-latest' + if: steps.opam-cache.outputs.cache-hit != 'true' && matrix.os != 'windows-latest' run: make install - name: Install deps (Windows with retry) - if: matrix.os == 'windows-latest' + if: steps.opam-cache.outputs.cache-hit != 'true' && matrix.os == 'windows-latest' shell: pwsh run: | $maxRetries = 3 @@ -99,7 +173,7 @@ jobs: run: make test - name: Install benchmark tools - if: ${{ matrix.os == 'ubuntu-latest' }} + if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y jq @@ -107,110 +181,122 @@ jobs: sudo dpkg -i hyperfine_1.18.0_amd64.deb - name: Run benchmarks - if: ${{ matrix.os == 'ubuntu-latest' }} + if: matrix.os == 'ubuntu-latest' run: BENCH_JQ=1 MIN_RUNS=5 ./benchmarks/bench.sh - name: Upload artifacts for ${{ matrix.os }}-${{ matrix.arch }} + if: matrix.os == 'ubuntu-latest' uses: actions/upload-artifact@v4 with: name: ${{ matrix.os }}-${{ matrix.arch }}-artifact path: _build/default/cli/cli.exe - name: Upload JavaScript artifacts for NPM - if: ${{ matrix.os == 'ubuntu-latest' }} + if: matrix.os == 'ubuntu-latest' uses: actions/upload-artifact@v4 with: name: query-json-js path: _build/default/js/js.bc.js - publish: - if: startsWith(github.ref, 'refs/tags/') || (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'pull_request' - needs: build - name: Publish and Deploy - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Use Node.js 22.x - uses: actions/setup-node@v4 - with: - node-version: 22.x - - - name: Use OCaml ${{ env.OCAML_COMPILER }} - uses: ocaml/setup-ocaml@v3 - with: - ocaml-compiler: ${{ env.OCAML_COMPILER }} - dune-cache: true - opam-disable-sandboxing: true - - - name: Pin some deps - run: make pin - - - name: Install opam deps - run: make install - - - name: Install npm deps - run: make npm-install - - - name: Build - run: make build-prod - - # Release to NPM and GitHub (only on tags) - - name: Download JavaScript artifact - if: startsWith(github.ref, 'refs/tags/') - uses: actions/download-artifact@v4 + - name: Save opam cache + uses: actions/cache/save@v5 + if: steps.opam-cache.outputs.cache-hit != 'true' with: - name: query-json-js - path: _release/query-json-js - - - name: Download linux artifacts - if: startsWith(github.ref, 'refs/tags/') - uses: actions/download-artifact@v4 - with: - name: ubuntu-latest-x64-artifact - path: _release/platform-linux-x64 + path: | + ${{ runner.tool_cache }}/opam + ~/.opam + _opam + .opam-path + C:\cygwin + C:\.opam + key: opam-${{ matrix.os }}-${{ matrix.arch }}-${{ env.OCAML_COMPILER }}-${{ hashFiles('dune-project', 'Makefile') }} + + # ======================================== + # Website deploy (ubuntu-latest, main/PR) + # ======================================== + - name: Build website + if: matrix.os == 'ubuntu-latest' && (github.ref == 'refs/heads/main' || github.event_name == 'pull_request') + run: make web-build - - name: Download macOS x64 artifacts - if: startsWith(github.ref, 'refs/tags/') - uses: actions/download-artifact@v4 + - name: Deploy to prod + if: matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/main' + uses: cloudflare/wrangler-action@v3 with: - name: macos-15-intel-x64-artifact - path: _release/platform-darwin-x64 + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy ../dist --project-name=query-json + workingDirectory: ./website - - name: Download macOS arm64 artifacts - if: startsWith(github.ref, 'refs/tags/') - uses: actions/download-artifact@v4 + - name: Deploy preview + if: matrix.os == 'ubuntu-latest' && github.event_name == 'pull_request' + uses: cloudflare/wrangler-action@v3 with: - name: macos-latest-arm64-artifact - path: _release/platform-darwin-arm64 + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy ../dist --project-name=query-json --branch=${{ github.head_ref }} + workingDirectory: ./website + gitHubToken: ${{ secrets.GITHUB_TOKEN }} - - name: Download windows artifacts - if: startsWith(github.ref, 'refs/tags/') + # ======================================== + # Release (ubuntu-latest, tags only) + # ======================================== + - name: Wait for other platform artifacts + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "Waiting for artifacts from other platforms..." + ARTIFACTS="macos-15-intel-x64-artifact macos-latest-arm64-artifact windows-latest-x64-artifact" + for artifact in $ARTIFACTS; do + echo "Waiting for $artifact..." + for i in {1..60}; do + if gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts | jq -e ".artifacts[] | select(.name == \"$artifact\")" > /dev/null 2>&1; then + echo "✓ $artifact is ready" + break + fi + if [ $i -eq 60 ]; then + echo "✗ Timeout waiting for $artifact" + exit 1 + fi + echo " Attempt $i/60, waiting 10s..." + sleep 10 + done + done + echo "All artifacts ready!" + + - name: Download all platform artifacts + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') uses: actions/download-artifact@v4 with: - name: windows-latest-x64-artifact - path: _release/platform-windows-x64 + path: _release - - name: Allow execution of query-json - if: startsWith(github.ref, 'refs/tags/') - run: chmod +x _release/platform-linux-x64/cli.exe + - name: Prepare release files + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') + run: | + # Reorganize downloaded artifacts + mkdir -p _release/platform-linux-x64 + mkdir -p _release/platform-darwin-x64 + mkdir -p _release/platform-darwin-arm64 + mkdir -p _release/platform-windows-x64 + mv _release/ubuntu-latest-x64-artifact/cli.exe _release/platform-linux-x64/ + mv _release/macos-15-intel-x64-artifact/cli.exe _release/platform-darwin-x64/ + mv _release/macos-latest-arm64-artifact/cli.exe _release/platform-darwin-arm64/ + mv _release/windows-latest-x64-artifact/cli.exe _release/platform-windows-x64/ + chmod +x _release/platform-linux-x64/cli.exe - name: Get version - if: startsWith(github.ref, 'refs/tags/') + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') id: version run: | VERSION=$(_release/platform-linux-x64/cli.exe --version) echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Generate NPM package - if: startsWith(github.ref, 'refs/tags/') + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') run: node scripts/make-npm-release.js ${{ steps.version.outputs.version }} - name: Check if version exists on NPM - if: startsWith(github.ref, 'refs/tags/') + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') id: npm_check run: | if npm view @davesnx/query-json@${{ steps.version.outputs.version }} version 2>/dev/null; then @@ -222,7 +308,7 @@ jobs: fi - name: Create archives for Github release - if: startsWith(github.ref, 'refs/tags/') + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') run: | cd _release/platform-darwin-x64 mv cli.exe query-json @@ -245,7 +331,7 @@ jobs: cd ../.. - name: Publish to NPM - if: startsWith(github.ref, 'refs/tags/') && steps.npm_check.outputs.should_publish_to_npm == 'true' + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') && steps.npm_check.outputs.should_publish_to_npm == 'true' run: | npm config set //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN npm config set scope "@davesnx" @@ -255,7 +341,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} - name: Create Github release - if: startsWith(github.ref, 'refs/tags/') + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') id: create_release uses: ncipollo/release-action@v1.16.0 env: @@ -268,7 +354,7 @@ jobs: makeLatest: true - name: Upload query-json-darwin-x64.zip to Github release - if: startsWith(github.ref, 'refs/tags/') + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -279,7 +365,7 @@ jobs: asset_content_type: application/zip - name: Upload query-json-darwin-arm64.zip to Github release - if: startsWith(github.ref, 'refs/tags/') + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -290,7 +376,7 @@ jobs: asset_content_type: application/zip - name: Upload query-json-linux-x64.zip to Github release - if: startsWith(github.ref, 'refs/tags/') + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -301,7 +387,7 @@ jobs: asset_content_type: application/zip - name: Upload query-json-windows-x64.zip to Github release - if: startsWith(github.ref, 'refs/tags/') + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -311,13 +397,12 @@ jobs: asset_name: query-json-windows-x64.zip asset_content_type: application/zip - # Publish to opam (only on tags) - name: Install dune-release - if: startsWith(github.ref, 'refs/tags/') + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') run: opam install dune-release -y - name: Create source distribution archive - if: startsWith(github.ref, 'refs/tags/') + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') run: | opam exec -- dune-release distrib -p query-json echo "Files created:" @@ -325,7 +410,7 @@ jobs: ls -la _build/*.tbz || echo "No .tbz files found in _build" - name: Upload source archive to Github release - if: startsWith(github.ref, 'refs/tags/') + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -336,7 +421,7 @@ jobs: asset_content_type: application/x-tar - name: Publish to opam - if: startsWith(github.ref, 'refs/tags/') + if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/') uses: davesnx/dune-release-action@v0.2.1 with: packages: 'query-json' @@ -344,27 +429,3 @@ jobs: github-token: ${{ secrets.GH_TOKEN }} to-opam-repository: true to-github-releases: false - - # Deploy website (only on main or PR) - - name: Build website - if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'pull_request' - run: make web-build - - - name: Deploy to prod - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: cloudflare/wrangler-action@v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy ../dist --project-name=query-json - workingDirectory: ./website - - - name: Deploy preview - if: github.event_name == 'pull_request' - uses: cloudflare/wrangler-action@v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy ../dist --project-name=query-json --branch=${{ github.head_ref }} - workingDirectory: ./website - gitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.ocamlformat b/.ocamlformat index e69de29..a7d2ee3 100644 --- a/.ocamlformat +++ b/.ocamlformat @@ -0,0 +1 @@ +ocaml-version = 5.4.0 diff --git a/CHANGES.md b/CHANGES.md index 06071d0..1f70464 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,7 @@ # Unreleased +## 1.0.0~beta-1 + - Rename functions for clarity - Deprecate `..` syntax, use `descend` - Improve REPL completions @@ -10,7 +12,6 @@ - Huge integers use arbitrary precision (`zarith`) for values beyond Int64 - `1152921504606846976 + 1` now correctly returns `1152921504606846977` (jq returns `1152921504606847000`) - Removed `Intlit`/`Floatlit`/`Stringlit` variants (string representations no longer needed) - - [BREAKING CHANGE] **Stricter null handling**: Operations on null now error instead of silently propagating - `null + 1` errors instead of returning `1` - `.missing.key` errors instead of returning `null` @@ -41,22 +42,19 @@ - [FEATURE] **Interactive REPL with autocomplete**: `query-json --repl data.json` - Context-aware suggestions based on data type - Tab completion for functions and keys + - Supports stdin input: `cat data.json | query-json --repl` - [FEATURE] **Built-in function reference**: `query-json --functions [category]` - [FEATURE] **`--null-input` / `-n` flag**: Run filter with `null` as input, useful for constructing JSON or as a calculator - [FEATURE] **Helpful error messages** with context, hints, and suggestions - Key not found shows available keys and suggests `?` - Type mismatch shows expected vs found types - Missing argument shows usage and examples - - [FEATURE] Functions added - **Traversal**: `descend`, `dive`, `find_all(expr)`, `find_first(expr)`, `paths_to(expr)` - **Collection**: `filter(expr)` (alias for `map(select(expr))`), `flat_map(expr)`, `find(expr)`, `some(expr)`, `pluck(expr)`, `partition(expr)` - **Predicates**: `is_empty`, `is_blank` - **Control flow**: `assert(cond)`, `assert(cond; msg)`, `raise(kind; msg)` - -### Improvements - -- [] Remove easy-format dependency +- Remove easy-format dependency - Add `=` assignment operator (`.a = .b`, `(.a, .b) = expr`) - Fix `|=` to create paths that don't exist (`.a |= 42` on `null` creates `{"a": 42}`) - Fix `|=` to take first value when transform produces multiple results diff --git a/README.md b/README.md index c712a31..a4c8d47 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,7 @@ A fast, friendly and portable JSON query language for the command line. **query-json** lets you slice, filter, and transform JSON data with a concise, expressive syntax. It started as a jq-compatible implementation, but evolved into something better: faster execution, better error messages, and a modernized language design. - - -## Why query-json? +## Why? ### Fast diff --git a/cli/Help.ml b/cli/Help.ml index 332883e..56be093 100644 --- a/cli/Help.ml +++ b/cli/Help.ml @@ -1,5 +1,4 @@ let find_group = Language.find_category -let available_categories = Language.category_names let format_function_doc ~colorize (f : Language.function_info) = let t = Console_style.make ~colorize in diff --git a/cli/cli.ml b/cli/cli.ml index 67f4e1b..7a1fec1 100644 --- a/cli/cli.ml +++ b/cli/cli.ml @@ -168,6 +168,9 @@ let execution position_0 position_1 verbose debug no_color raw_output null_input | Error err -> print_error_message ~colorize err) let () = + (* On Windows, stdout defaults to text mode which converts \n to \r\n. + Set binary mode to ensure consistent LF output across platforms. *) + set_binary_mode_out stdout true; let open Cmdliner.Arg in let query = value & pos 0 (some string) None & info [] ~doc:"Query to run" in let json = value & pos 1 (some string) None & info [] ~doc:"JSON" in diff --git a/cli/test/errors.t b/cli/test/errors.t index 7e43481..ffe3191 100644 --- a/cli/test/errors.t +++ b/cli/test/errors.t @@ -1,27 +1,27 @@ Deprecation errors for old function names tonumber is deprecated - $ echo '"42"' | query-json --no-color 'tonumber' + $ query-json -n --no-color 'tonumber' error[deprecated]: `tonumber` is deprecated --> tonumber - ^ + ^^^^^^^^ hint: use `to_number` instead tostring is deprecated - $ echo '42' | query-json --no-color 'tostring' + $ query-json -n --no-color 'tostring' error[deprecated]: `tostring` is deprecated --> tostring - ^ + ^^^^^^^^ hint: use `to_string` instead startwith is undefined (typo) - $ echo '"Hello, world"' | query-json --no-color 'startwith("Hello")' + $ query-json --no-color 'startwith("Hello")' '"Hello, world"' error[undefined_function]: undefined function: `startwith` @@ -29,17 +29,17 @@ startwith is undefined (typo) startswith is deprecated - $ echo '"Hello, world"' | query-json --no-color 'startswith("Hello")' + $ query-json -n --no-color 'startswith("Hello")' '"Hello, world"' error[deprecated]: `startswith` is deprecated --> startswith("Hello") - ^ + ^^^^^^^^^^^ hint: use `starts_with` instead endwith is undefined (typo) - $ echo '"Hello, world"' | query-json --no-color 'endwith("world")' + $ query-json -n --no-color 'endwith("world")' '"Hello, world"' error[undefined_function]: undefined function: `endwith` @@ -47,66 +47,79 @@ endwith is undefined (typo) endswith is deprecated - $ echo '"Hello, world"' | query-json --no-color 'endswith("world")' + $ query-json -n --no-color 'endswith("world")' '"Hello, world"' error[deprecated]: `endswith` is deprecated --> endswith("world") - ^ + ^^^^^^^^^ hint: use `ends_with` instead isnormal is deprecated - $ echo '42' | query-json --no-color 'isnormal' + $ query-json -n --no-color 'isnormal' error[deprecated]: `isnormal` is deprecated --> isnormal - ^ + ^^^^^^^^ hint: use `is_normal` instead trim_left is deprecated - $ echo '" hello "' | query-json --no-color 'trim_left' + $ query-json -n --no-color 'trim_left' '" hello "' error[deprecated]: `trim_left` is deprecated --> trim_left - ^ + ^^^^^^^^^ hint: use `trim` instead trim_right is deprecated - $ echo '" hello "' | query-json --no-color 'trim_right' + $ query-json -n --no-color 'trim_right' '" hello "' error[deprecated]: `trim_right` is deprecated --> trim_right - ^ + ^^^^^^^^^^ hint: use `trim` instead +map without expression + $ query-json --no-color 'map' '[1,2,3]' + + error[missing_argument]: map() requires expr + --> map + ^^^ + + usage: map(expr) + Transform each element + applicable to: array + example: [1, 2, 3] | map(. * 2) → [2, 4, 6] + + New names work correctly - $ echo '"42"' | query-json --no-color 'to_number' + $ query-json --no-color 'to_number' '"42"' 42 - $ echo '42' | query-json --no-color 'to_string' + $ query-json --no-color 'to_string' '42' "42" - $ echo '"Hello, world"' | query-json --no-color 'starts_with("Hello")' + $ query-json --no-color 'starts_with("Hello")' '"Hello, world"' true - $ echo '"Hello, world"' | query-json --no-color 'starts_with("boo")' + $ query-json --no-color 'starts_with("boo")' '"Hello, world"' false - $ echo '"Hello, world"' | query-json --no-color 'ends_with("world")' + $ query-json --no-color 'ends_with("world")' '"Hello, world"' true - $ echo '42' | query-json --no-color 'is_normal' + $ query-json --no-color 'is_normal' '42' true - $ echo '" hello "' | query-json --no-color 'trim' + $ query-json --no-color 'trim' '" hello "' "hello" Hyphenated keys should suggest quoted syntax diff --git a/docs/JQ_COMPATIBILITY.md b/docs/JQ_COMPATIBILITY.md index ebadd09..87088f9 100644 --- a/docs/JQ_COMPATIBILITY.md +++ b/docs/JQ_COMPATIBILITY.md @@ -26,6 +26,9 @@ While query-json implements most of jq's functionality, v1 introduces intentiona - [Core Functions](#core-functions) - [Not Implemented](#not-implemented) - [Additional Features](#additional-features-in-query-json) +- [Video Demos](#video-demos) + - [Available Demos](#available-demos) + - [Generating Videos](#generating-videos) --- @@ -453,3 +456,42 @@ query-json includes some functions not found in jq: | `dive` | Depth-first traversal | `[dive \| strings]` | | `find_all(expr)` | Find all matching at any depth | `find_all(type=="number")` | | `paths_to(expr)` | Get paths to all matching values | `paths_to(type=="string")` | + +--- + +## Video Demos + +All the examples in this guide have corresponding VHS tape demos. You can find them in [`docs/jq-compat-demos/`](./jq-compat-demos/) and generate the videos using [VHS](https://github.com/charmbracelet/vhs). + +### Available Demos + +| Demo | Description | Tape File | +|------|-------------|-----------| +| **Error Messages** | Helpful error messages with context and hints | [`00-error-messages.tape`](./jq-compat-demos/00-error-messages.tape) | +| **Null Handling** | Stricter null handling catches bugs early | [`01-stricter-null-handling.tape`](./jq-compat-demos/01-stricter-null-handling.tape) | +| **fn vs def** | `fn` keyword for user-defined functions | [`02-fn-vs-def.tape`](./jq-compat-demos/02-fn-vs-def.tape) | +| **Snake_case** | Readable `snake_case` function names | [`03-snake-case-naming.tape`](./jq-compat-demos/03-snake-case-naming.tape) | +| **Clearer Naming** | Self-explanatory function names | [`04-clearer-naming.tape`](./jq-compat-demos/04-clearer-naming.tape) | +| **group_by** | Returns object instead of array of arrays | [`05-group-by-behavior.tape`](./jq-compat-demos/05-group-by-behavior.tape) | +| **keys** | Preserves insertion order | [`06-keys-insertion-order.tape`](./jq-compat-demos/06-keys-insertion-order.tape) | +| **unique** | Preserves insertion order | [`07-unique-insertion-order.tape`](./jq-compat-demos/07-unique-insertion-order.tape) | +| **infinite** | Generator producing 0, 1, 2, ... | [`08-infinite-generator.tape`](./jq-compat-demos/08-infinite-generator.tape) | +| **Optional Functions** | `?` works on function calls | [`09-optional-access-functions.tape`](./jq-compat-demos/09-optional-access-functions.tape) | +| **Additional Features** | Features unique to query-json | [`10-additional-features.tape`](./jq-compat-demos/10-additional-features.tape) | +| **All Demos** | Complete overview of all differences | [`all-demos.tape`](./jq-compat-demos/all-demos.tape) | + +### Generating Videos + +To generate all demo videos: + +```bash +# Generate all demos +for tape in docs/jq-compat-demos/*.tape; do + vhs "$tape" +done + +# Or generate a specific demo +vhs docs/jq-compat-demos/all-demos.tape +``` + +The generated `.mp4` files will be placed in `docs/jq-compat-demos/`. diff --git a/docs/jq-compat-demos/00-error-messages.tape b/docs/jq-compat-demos/00-error-messages.tape new file mode 100644 index 0000000..50e3b2a --- /dev/null +++ b/docs/jq-compat-demos/00-error-messages.tape @@ -0,0 +1,66 @@ +# query-json: Helpful Error Messages +# Shows how query-json gives context and hints + +Output docs/jq-compat-demos/00-error-messages.mp4 +Require query-json + +Set FontSize 24 +Set Width 1200 +Set Height 800 +Set FontFamily "IBM Plex Mono" +Set CursorBlink false + +Type "# query-json: Helpful Error Messages" +Enter +Sleep 1s + +# Missing key with suggestions +Type "# 1. Missing key with suggestions" +Enter +Sleep 500ms +Type `query-json '.naem' '{"name": "Alice", "age": 30}'` +Enter +Sleep 3s + +# Type mismatch +Type "# 2. Type mismatch with context" +Enter +Sleep 500ms +Type `query-json '.age | length' '{"name": "Alice", "age": 30}'` +Enter +Sleep 3s + +# Index out of bounds +Type "# 3. Index out of bounds" +Enter +Sleep 500ms +Type `query-json '.[10]' '[1, 2, 3]'` +Enter +Sleep 3s + +# Missing function argument +Type "# 4. Missing function argument" +Enter +Sleep 500ms +Type `echo '[1,2,3]' | query-json 'map'` +Enter +Sleep 3s + +# Null access +Type "# 5. Null access" +Enter +Sleep 500ms +Type `query-json '.missing.name' '{"other": 1}'` +Enter +Sleep 3s + +# Parse error +Type "# 6. Parse error with location" +Enter +Sleep 500ms +Type `query-json '{name: .foo}' '{"foo": 1}'` +Enter +Sleep 3s + +Sleep 1s + diff --git a/docs/jq-compat-demos/01-stricter-null-handling.tape b/docs/jq-compat-demos/01-stricter-null-handling.tape new file mode 100644 index 0000000..cdd505f --- /dev/null +++ b/docs/jq-compat-demos/01-stricter-null-handling.tape @@ -0,0 +1,72 @@ +# query-json vs jq: Stricter Null Handling +# Demonstrates how query-json catches null-related bugs early + +Output docs/jq-compat-demos/01-stricter-null-handling.mp4 +Require query-json + +Set FontSize 28 +Set Width 1200 +Set Height 700 +Set FontFamily "IBM Plex Mono" +Set CursorBlink false + +Type "# query-json: Stricter Null Handling" +Enter +Sleep 1s + +# Null arithmetic comparison +Type "# 1. Null arithmetic" +Enter +Sleep 500ms +Type "# jq silently coerces null to 0:" +Enter +Type "echo 'null' | jq '. + 1'" +Enter +Sleep 2s + +Type "# query-json raises an error:" +Enter +Type "echo 'null' | query-json '. + 1'" +Enter +Sleep 2.5s + +# Null key access comparison +Type "# 2. Null key access" +Enter +Sleep 500ms +Type "# jq silently returns null:" +Enter +Type `echo '{"a": null}' | jq '.a.b.c'` +Enter +Sleep 2s + +Type "# query-json tells you what happened:" +Enter +Type `echo '{"a": null}' | query-json '.a.b'` +Enter +Sleep 2.5s + +# Optional access when null is expected +Type "# 3. Use ? for optional access" +Enter +Sleep 500ms +Type "# Optional access returns null without error:" +Enter +Type `query-json '.missing?' '{"name": "Alice"}'` +Enter +Sleep 2s + +Type `query-json '.a.b?' '{"a": null}'` +Enter +Sleep 2s + +# Try-catch for complex error handling +Type "# 4. Try-catch for complex error handling" +Enter +Sleep 500ms +Type `query-json 'try .foo.bar catch "not found"' '{"other": 1}'` +Enter +Sleep 2s + +Sleep 1s + diff --git a/docs/jq-compat-demos/02-fn-vs-def.tape b/docs/jq-compat-demos/02-fn-vs-def.tape new file mode 100644 index 0000000..80b089f --- /dev/null +++ b/docs/jq-compat-demos/02-fn-vs-def.tape @@ -0,0 +1,65 @@ +# query-json vs jq: Function Definition Syntax +# Shows fn keyword instead of def + +Output docs/jq-compat-demos/02-fn-vs-def.mp4 +Require query-json + +Set FontSize 28 +Set Width 1200 +Set Height 700 +Set FontFamily "IBM Plex Mono" +Set CursorBlink false + +Type "# query-json: fn for User-Defined Functions" +Enter +Sleep 1s + +# Basic function definition +Type "# jq uses 'def':" +Enter +Sleep 500ms +Type "echo '[1,2,3]' | jq 'def double: . * 2; map(double)'" +Enter +Sleep 2s + +Type "# query-json uses 'fn':" +Enter +Sleep 500ms +Type "echo '[1,2,3]' | query-json 'fn double: . * 2; map(double)'" +Enter +Sleep 2s + +# Function with arguments +Type "# Function with arguments" +Enter +Sleep 500ms +Type "# jq:" +Enter +Type `echo '[1,2,3]' | jq 'def add_n(n): . + n; map(add_n(10))'` +Enter +Sleep 2s + +Type "# query-json:" +Enter +Type `echo '[1,2,3]' | query-json 'fn add_n(n): . + n; map(add_n(10))'` +Enter +Sleep 2s + +# Recursive function +Type "# Recursive function (factorial)" +Enter +Sleep 500ms +Type `query-json 'fn fact: if . <= 1 then 1 else . * ((. - 1) | fact) end; 5 | fact' 'null'` +Enter +Sleep 2.5s + +# Multiple functions +Type "# Multiple functions" +Enter +Sleep 500ms +Type `query-json 'fn square: . * .; fn sum_of_squares: map(square) | add; [1,2,3,4] | sum_of_squares' 'null'` +Enter +Sleep 2.5s + +Sleep 1s + diff --git a/docs/jq-compat-demos/03-snake-case-naming.tape b/docs/jq-compat-demos/03-snake-case-naming.tape new file mode 100644 index 0000000..944cc47 --- /dev/null +++ b/docs/jq-compat-demos/03-snake-case-naming.tape @@ -0,0 +1,98 @@ +# query-json vs jq: Snake_case Naming +# Shows the more readable function naming convention + +Output docs/jq-compat-demos/03-snake-case-naming.mp4 +Require query-json + +Set FontSize 28 +Set Width 1200 +Set Height 700 +Set FontFamily "IBM Plex Mono" +Set CursorBlink false + +Type "# query-json: Snake_case Function Names" +Enter +Sleep 1s + +# Type conversion +Type "# 1. Type conversion" +Enter +Sleep 500ms +Type "# jq: tostring / tonumber" +Enter +Type "echo '42' | jq 'tostring'" +Enter +Sleep 1.5s + +Type "# query-json: to_string / to_number" +Enter +Type "echo '42' | query-json 'to_string'" +Enter +Sleep 2s + +# String predicates +Type "# 2. String predicates" +Enter +Sleep 500ms +Type "# jq: startswith / endswith" +Enter +Type `echo '"hello world"' | jq 'startswith("hello")'` +Enter +Sleep 1.5s + +Type "# query-json: starts_with / ends_with" +Enter +Type `echo '"hello world"' | query-json 'starts_with("hello")'` +Enter +Sleep 2s + +# Type checking +Type "# 3. Type checking" +Enter +Sleep 500ms +Type "# jq: isnan" +Enter +Type `echo '"not a number"' | jq 'try tonumber catch nan | isnan'` +Enter +Sleep 2s + +Type "# query-json: is_nan" +Enter +Type `query-json 'nan | is_nan' 'null'` +Enter +Sleep 2s + +# Emptiness checks +Type "# 4. Emptiness checks" +Enter +Sleep 500ms +Type "# jq: isempty" +Enter +Type `echo '[]' | jq 'isempty(.[])'` +Enter +Sleep 1.5s + +Type "# query-json: is_empty" +Enter +Type `echo '[]' | query-json 'is_empty(.[])'` +Enter +Sleep 2s + +# Path operations +Type "# 5. Path operations" +Enter +Sleep 500ms +Type "# jq: getpath / setpath" +Enter +Type `echo '{"a":{"b":1}}' | jq 'getpath(["a","b"])'` +Enter +Sleep 1.5s + +Type "# query-json: get_path / set_path" +Enter +Type `echo '{"a":{"b":1}}' | query-json 'get_path(["a","b"])'` +Enter +Sleep 2s + +Sleep 1s + diff --git a/docs/jq-compat-demos/04-clearer-naming.tape b/docs/jq-compat-demos/04-clearer-naming.tape new file mode 100644 index 0000000..59cbbd9 --- /dev/null +++ b/docs/jq-compat-demos/04-clearer-naming.tape @@ -0,0 +1,86 @@ +# query-json vs jq: Clearer Naming +# Shows self-explanatory function names + +Output docs/jq-compat-demos/04-clearer-naming.mp4 +Require query-json + +Set FontSize 28 +Set Width 1200 +Set Height 700 +Set FontFamily "IBM Plex Mono" +Set CursorBlink false + +Type "# query-json: Clearer Function Names" +Enter +Sleep 1s + +# Case conversion +Type "# 1. Case conversion" +Enter +Sleep 500ms +Type "# jq: ascii_upcase / ascii_downcase" +Enter +Type `echo '"Hello World"' | jq 'ascii_upcase'` +Enter +Sleep 1.5s + +Type "# query-json: to_uppercase / to_lowercase" +Enter +Type `echo '"Hello World"' | query-json 'to_uppercase'` +Enter +Sleep 2s + +# String trimming +Type "# 2. String trimming" +Enter +Sleep 500ms +Type "# jq: ltrimstr / rtrimstr (l=left, r=right)" +Enter +Type `echo '"hello world"' | jq 'ltrimstr("hello ")'` +Enter +Sleep 1.5s + +Type "# query-json: trim_start / trim_end" +Enter +Type `echo '"hello world"' | query-json 'trim_start("hello ")'` +Enter +Sleep 2s + +Type `echo '"hello world"' | query-json 'trim_end(" world")'` +Enter +Sleep 2s + +# Byte length +Type "# 3. Byte length" +Enter +Sleep 500ms +Type "# jq: utf8bytelength" +Enter +Type `echo '"café"' | jq 'utf8bytelength'` +Enter +Sleep 1.5s + +Type "# query-json: byte_length" +Enter +Type `echo '"café"' | query-json 'byte_length'` +Enter +Sleep 2s + +# Combined example +Type "# 4. Combined: A real-world example" +Enter +Sleep 500ms +Type "# jq:" +Enter +Type `echo '{"name":"john doe"}' | jq '.name | ascii_upcase | startswith("JOHN")'` +Enter +Sleep 2s + +Type "# query-json:" +Enter +Type `echo '{"name":"john doe"}' | query-json '.name | to_uppercase | starts_with("JOHN")'` +Enter +Sleep 2s + +Sleep 1s + diff --git a/docs/jq-compat-demos/05-group-by-behavior.tape b/docs/jq-compat-demos/05-group-by-behavior.tape new file mode 100644 index 0000000..1545a01 --- /dev/null +++ b/docs/jq-compat-demos/05-group-by-behavior.tape @@ -0,0 +1,57 @@ +# query-json vs jq: group_by Behavior +# Shows how group_by returns an object instead of array of arrays + +Output docs/jq-compat-demos/05-group-by-behavior.mp4 +Require query-json + +Set FontSize 28 +Set Width 1200 +Set Height 700 +Set FontFamily "IBM Plex Mono" +Set CursorBlink false + +Type "# query-json: group_by Returns an Object" +Enter +Sleep 1s + +# Basic group_by difference +Type "# jq returns array of arrays:" +Enter +Sleep 500ms +Type `echo '[{"x":1},{"x":2},{"x":1}]' | jq 'group_by(.x)'` +Enter +Sleep 2.5s + +Type "# query-json returns an object:" +Enter +Sleep 500ms +Type `echo '[{"x":1},{"x":2},{"x":1}]' | query-json 'group_by(.x)'` +Enter +Sleep 2.5s + +# More practical example +Type "# Practical example: Group users by role" +Enter +Sleep 500ms +Type `echo '[{"name":"Alice","role":"admin"},{"name":"Bob","role":"user"},{"name":"Carol","role":"admin"}]' | query-json 'group_by(.role)'` +Enter +Sleep 3s + +# Access groups by key +Type "# Access groups directly by key:" +Enter +Sleep 500ms +Type `echo '[{"name":"Alice","role":"admin"},{"name":"Bob","role":"user"}]' | query-json 'group_by(.role).admin'` +Enter +Sleep 2.5s + +# Migrating from jq's structure +Type "# To get jq-style structure:" +Enter +Sleep 500ms +Type `echo '[{"x":1},{"x":2},{"x":1}]' | query-json 'group_by(.x) | to_entries | map({key, items: .value})'` +Enter +Sleep 3s + +Sleep 1s + diff --git a/docs/jq-compat-demos/06-keys-insertion-order.tape b/docs/jq-compat-demos/06-keys-insertion-order.tape new file mode 100644 index 0000000..6d9428d --- /dev/null +++ b/docs/jq-compat-demos/06-keys-insertion-order.tape @@ -0,0 +1,56 @@ +# query-json vs jq: keys Preserves Insertion Order +# Shows how keys returns insertion order instead of sorted + +Output docs/jq-compat-demos/06-keys-insertion-order.mp4 +Require query-json + +Set FontSize 28 +Set Width 1200 +Set Height 700 +Set FontFamily "IBM Plex Mono" +Set CursorBlink false + +Type "# query-json: keys Preserves Insertion Order" +Enter +Sleep 1s + +# Basic keys difference +Type "# jq returns sorted keys:" +Enter +Sleep 500ms +Type `echo '{"b":1, "a":2, "c":3}' | jq 'keys'` +Enter +Sleep 2s + +Type "# query-json preserves insertion order:" +Enter +Sleep 500ms +Type `echo '{"b":1, "a":2, "c":3}' | query-json 'keys'` +Enter +Sleep 2s + +# jq's keys_unsorted equivalent +Type "# jq requires keys_unsorted for insertion order" +Enter +Sleep 500ms +Type `echo '{"z":1, "m":2, "a":3}' | jq 'keys_unsorted'` +Enter +Sleep 2s + +Type "# query-json: just use keys" +Enter +Sleep 500ms +Type `echo '{"z":1, "m":2, "a":3}' | query-json 'keys'` +Enter +Sleep 2s + +# If you need sorted keys +Type "# If you need sorted keys in query-json:" +Enter +Sleep 500ms +Type `echo '{"b":1, "a":2, "c":3}' | query-json 'keys | sort'` +Enter +Sleep 2s + +Sleep 1s + diff --git a/docs/jq-compat-demos/07-unique-insertion-order.tape b/docs/jq-compat-demos/07-unique-insertion-order.tape new file mode 100644 index 0000000..f161d7a --- /dev/null +++ b/docs/jq-compat-demos/07-unique-insertion-order.tape @@ -0,0 +1,61 @@ +# query-json vs jq: unique Preserves Insertion Order +# Shows how unique returns first occurrences in order + +Output docs/jq-compat-demos/07-unique-insertion-order.mp4 +Require query-json + +Set FontSize 28 +Set Width 1200 +Set Height 700 +Set FontFamily "IBM Plex Mono" +Set CursorBlink false + +Type "# query-json: unique Preserves Insertion Order" +Enter +Sleep 1s + +# Basic unique difference +Type "# jq sorts the result:" +Enter +Sleep 500ms +Type "echo '[3, 1, 2, 1, 3, 2]' | jq 'unique'" +Enter +Sleep 2s + +Type "# query-json preserves insertion order:" +Enter +Sleep 500ms +Type "echo '[3, 1, 2, 1, 3, 2]' | query-json 'unique'" +Enter +Sleep 2s + +# With strings +Type "# With strings:" +Enter +Sleep 500ms +Type `echo '["banana", "apple", "cherry", "apple", "banana"]' | jq 'unique'` +Enter +Sleep 2s + +Type `echo '["banana", "apple", "cherry", "apple", "banana"]' | query-json 'unique'` +Enter +Sleep 2s + +# If you need sorted unique +Type "# If you need sorted unique in query-json:" +Enter +Sleep 500ms +Type "echo '[3, 1, 2, 1, 3, 2]' | query-json 'unique | sort'" +Enter +Sleep 2s + +# unique_by also preserves order +Type "# unique_by also preserves order:" +Enter +Sleep 500ms +Type `echo '[{"name":"Bob","id":1},{"name":"Alice","id":2},{"name":"Bob","id":3}]' | query-json 'unique_by(.name)'` +Enter +Sleep 2.5s + +Sleep 1s + diff --git a/docs/jq-compat-demos/08-infinite-generator.tape b/docs/jq-compat-demos/08-infinite-generator.tape new file mode 100644 index 0000000..6189374 --- /dev/null +++ b/docs/jq-compat-demos/08-infinite-generator.tape @@ -0,0 +1,66 @@ +# query-json: infinite is a Generator +# Shows how infinite produces 0, 1, 2, ... forever + +Output docs/jq-compat-demos/08-infinite-generator.mp4 +Require query-json + +Set FontSize 28 +Set Width 1200 +Set Height 700 +Set FontFamily "IBM Plex Mono" +Set CursorBlink false + +Type "# query-json: infinite is a Generator" +Enter +Sleep 1s + +# Basic infinite with limit +Type "# Generate first 5 numbers:" +Enter +Sleep 500ms +Type "query-json '[limit(5; infinite)]' 'null'" +Enter +Sleep 2s + +# Generate more +Type "# Generate first 10 numbers:" +Enter +Sleep 500ms +Type "query-json '[limit(10; infinite)]' 'null'" +Enter +Sleep 2s + +# Use with first +Type "# Get first value:" +Enter +Sleep 500ms +Type "query-json 'first(infinite)' 'null'" +Enter +Sleep 2s + +# Use with nth +Type "# Get nth value:" +Enter +Sleep 500ms +Type "query-json 'nth(5; infinite)' 'null'" +Enter +Sleep 2s + +# Transform infinite sequence +Type "# Transform the sequence:" +Enter +Sleep 500ms +Type "query-json '[limit(5; infinite) | . * 2]' 'null'" +Enter +Sleep 2s + +# Filter infinite sequence +Type "# Filter: first 5 even numbers from infinite:" +Enter +Sleep 500ms +Type "query-json '[limit(5; infinite | select(. % 2 == 0))]' 'null'" +Enter +Sleep 2.5s + +Sleep 1s + diff --git a/docs/jq-compat-demos/09-optional-access-functions.tape b/docs/jq-compat-demos/09-optional-access-functions.tape new file mode 100644 index 0000000..408214d --- /dev/null +++ b/docs/jq-compat-demos/09-optional-access-functions.tape @@ -0,0 +1,75 @@ +# query-json: Optional Access on Functions +# Shows how ? works on function calls, not just field access + +Output docs/jq-compat-demos/09-optional-access-functions.mp4 +Require query-json + +Set FontSize 28 +Set Width 1200 +Set Height 700 +Set FontFamily "IBM Plex Mono" +Set CursorBlink false + +Type "# query-json: Optional Access on Functions" +Enter +Sleep 1s + +Type "# In jq, ? only works on field access and indexing" +Enter +Type "# In query-json, ? works on ANY expression!" +Enter +Sleep 1.5s + +# first? on empty array +Type "# Optional first on empty array:" +Enter +Sleep 500ms +Type `query-json 'first?' '[]'` +Enter +Sleep 2s + +# keys? on non-object +Type "# Optional keys on non-object:" +Enter +Sleep 500ms +Type `query-json 'keys?' '123'` +Enter +Sleep 2s + +# nth? out of bounds +Type "# Optional nth out of bounds:" +Enter +Sleep 500ms +Type `query-json 'nth(10)?' '[1,2,3]'` +Enter +Sleep 2s + +# first? with empty generator +Type "# Optional first with empty generator:" +Enter +Sleep 500ms +Type `query-json 'first?(empty)' 'null'` +Enter +Sleep 2s + +# Both syntaxes work +Type "# Both syntaxes work:" +Enter +Sleep 500ms +Type `query-json 'first?(range(3))' 'null'` +Enter +Sleep 1.5s +Type `query-json 'first(range(3))?' 'null'` +Enter +Sleep 2s + +# Practical use case +Type "# Practical: Safe array access" +Enter +Sleep 500ms +Type `query-json '.users | first? // "no users"' '{"users": []}'` +Enter +Sleep 2s + +Sleep 1s + diff --git a/docs/jq-compat-demos/10-additional-features.tape b/docs/jq-compat-demos/10-additional-features.tape new file mode 100644 index 0000000..0d8159d --- /dev/null +++ b/docs/jq-compat-demos/10-additional-features.tape @@ -0,0 +1,88 @@ +# query-json: Additional Features Not in jq +# Shows functions unique to query-json + +Output docs/jq-compat-demos/10-additional-features.mp4 +Require query-json + +Set FontSize 28 +Set Width 1200 +Set Height 700 +Set FontFamily "IBM Plex Mono" +Set CursorBlink false + +Type "# query-json: Additional Features" +Enter +Sleep 1s + +# filter - alias for map(select()) +Type "# filter(expr) - alias for map(select(expr))" +Enter +Sleep 500ms +Type `echo '[1,2,3,4,5]' | query-json 'filter(. > 2)'` +Enter +Sleep 2s + +# flat_map +Type "# flat_map(expr) - map and flatten" +Enter +Sleep 500ms +Type `echo '[[1,2],[3,4]]' | query-json 'flat_map(. | map(. * 2))'` +Enter +Sleep 2s + +# find +Type "# find(expr) - first matching element" +Enter +Sleep 500ms +Type `echo '[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]' | query-json 'find(.id == 2)'` +Enter +Sleep 2.5s + +# some +Type "# some(expr) - check if any element matches" +Enter +Sleep 500ms +Type `echo '[{"price":50},{"price":150}]' | query-json 'some(.price > 100)'` +Enter +Sleep 2s + +# pluck +Type "# pluck(expr) - extract field from array" +Enter +Sleep 500ms +Type `echo '[{"name":"Alice"},{"name":"Bob"}]' | query-json 'pluck(.name)'` +Enter +Sleep 2s + +# partition +Type "# partition(expr) - split into matching/non-matching" +Enter +Sleep 500ms +Type `echo '[1,2,3,4,5]' | query-json 'partition(. > 2)'` +Enter +Sleep 2.5s + +# is_empty +Type "# is_empty - check if empty" +Enter +Sleep 500ms +Type `echo '[]' | query-json 'is_empty'` +Enter +Sleep 1.5s +Type `echo '[1]' | query-json 'is_empty'` +Enter +Sleep 2s + +# is_blank +Type "# is_blank - null, empty, or whitespace" +Enter +Sleep 500ms +Type `echo '" "' | query-json 'is_blank'` +Enter +Sleep 1.5s +Type `echo '""' | query-json 'is_blank'` +Enter +Sleep 2s + +Sleep 1s + diff --git a/docs/jq-compat-demos/all-demos.tape b/docs/jq-compat-demos/all-demos.tape new file mode 100644 index 0000000..cc08b7f --- /dev/null +++ b/docs/jq-compat-demos/all-demos.tape @@ -0,0 +1,121 @@ +# query-json: Complete JQ Compatibility Demo +# Shows all major differences and features + +Output docs/jq-compat-demos/all-demos.mp4 +Require query-json + +Set FontSize 26 +Set Width 1200 +Set Height 700 +Set FontFamily "IBM Plex Mono" +Set CursorBlink false + +# Title +Type "# query-json vs jq: Complete Compatibility Guide" +Enter +Sleep 1.5s + +# Section 1: Error Messages +Type "# === HELPFUL ERROR MESSAGES ===" +Enter +Sleep 500ms +Type `query-json '.naem' '{"name": "Alice", "age": 30}'` +Enter +Sleep 2.5s + +# Section 2: Stricter Null Handling +Type "# === STRICTER NULL HANDLING ===" +Enter +Sleep 500ms +Type "# jq silently coerces null:" +Enter +Type "echo 'null' | jq '. + 1'" +Enter +Sleep 1.5s +Type "# query-json catches the bug:" +Enter +Type "echo 'null' | query-json '. + 1'" +Enter +Sleep 2s + +# Section 3: fn vs def +Type "# === FN FOR FUNCTIONS (not def) ===" +Enter +Sleep 500ms +Type "echo '[1,2,3]' | query-json 'fn double: . * 2; map(double)'" +Enter +Sleep 2s + +# Section 4: Snake case +Type "# === SNAKE_CASE NAMING ===" +Enter +Sleep 500ms +Type `echo '"hello"' | query-json 'to_uppercase'` +Enter +Sleep 1.5s +Type `echo '"hello world"' | query-json 'starts_with("hello")'` +Enter +Sleep 2s + +# Section 5: Clearer naming +Type "# === CLEARER NAMES ===" +Enter +Sleep 500ms +Type `echo '" hello "' | query-json 'trim'` +Enter +Sleep 1.5s +Type `echo '"hello world"' | query-json 'trim_start("hello ")'` +Enter +Sleep 2s + +# Section 6: group_by returns object +Type "# === GROUP_BY RETURNS OBJECT ===" +Enter +Sleep 500ms +Type `echo '[{"x":1},{"x":2},{"x":1}]' | query-json 'group_by(.x)'` +Enter +Sleep 2.5s + +# Section 7: keys preserves order +Type "# === KEYS PRESERVES INSERTION ORDER ===" +Enter +Sleep 500ms +Type `echo '{"b":1, "a":2, "c":3}' | query-json 'keys'` +Enter +Sleep 2s + +# Section 8: unique preserves order +Type "# === UNIQUE PRESERVES ORDER ===" +Enter +Sleep 500ms +Type "echo '[3, 1, 2, 1, 3, 2]' | query-json 'unique'" +Enter +Sleep 2s + +# Section 9: Optional access on functions +Type "# === OPTIONAL ACCESS ON FUNCTIONS ===" +Enter +Sleep 500ms +Type `query-json 'first?' '[]'` +Enter +Sleep 1.5s +Type `query-json 'keys?' '123'` +Enter +Sleep 2s + +# Section 10: Additional features +Type "# === ADDITIONAL FEATURES ===" +Enter +Sleep 500ms +Type `echo '[1,2,3,4,5]' | query-json 'filter(. > 2)'` +Enter +Sleep 1.5s +Type `echo '[{"id":1},{"id":2}]' | query-json 'find(.id == 2)'` +Enter +Sleep 1.5s +Type `echo '[{"name":"Alice"},{"name":"Bob"}]' | query-json 'pluck(.name)'` +Enter +Sleep 2s + +Sleep 1s + diff --git a/js/Js.ml b/js/Js.ml index 177607e..82b1036 100644 --- a/js/Js.ml +++ b/js/Js.ml @@ -97,7 +97,7 @@ let get_categories () = let () = Js_of_ocaml.Js.export "query-json" - (object%js - val run = Js_of_ocaml.Js.wrap_callback run - val categories = Js_of_ocaml.Js.wrap_callback get_categories - end) + object%js + val run = Js_of_ocaml.Js.wrap_callback run + val categories = Js_of_ocaml.Js.wrap_callback get_categories + end diff --git a/source/Ast.ml b/source/Ast.ml index dac6db6..9c0fd5c 100644 --- a/source/Ast.ml +++ b/source/Ast.ml @@ -181,7 +181,6 @@ type fn1_expr = type compiled_regex = { pattern : string; regex : Str.regexp } -let show_compiled_regex r = Printf.sprintf "/%s/" r.pattern let pp_compiled_regex fmt r = Format.fprintf fmt "/%s/" r.pattern type fn2 = diff --git a/source/Core.ml b/source/Core.ml index 8813ac4..4fda4ce 100644 --- a/source/Core.ml +++ b/source/Core.ml @@ -9,11 +9,17 @@ let last_position = ref Location.none exception Lexer_error of string let provider ~debug buf = - let start, stop = Sedlexing.lexing_positions buf in - last_position := { loc_start = start; loc_end = stop }; + let start, _ = Sedlexing.lexing_positions buf in let token = - match Lexer.tokenize buf with Ok t -> t | Error e -> raise (Lexer_error e) + match Lexer.tokenize buf with + | Ok t -> t + | Error e -> + let _, stop = Sedlexing.lexing_positions buf in + last_position := { loc_start = start; loc_end = stop }; + raise (Lexer_error e) in + let _, stop = Sedlexing.lexing_positions buf in + last_position := { loc_start = start; loc_end = stop }; if debug then print_endline (Lexer.show_token token); (token, start, stop) @@ -24,15 +30,6 @@ let position_to_string start end_ = (start.Lexing.pos_cnum - start.Lexing.pos_bol) (end_.Lexing.pos_cnum - end_.Lexing.pos_bol) -let pretty_print_error ~colorize ~input ~(start : Lexing.position) - ~(end_ : Lexing.position) = - let err = - Query_error.parse_error - ~message:("problem parsing at " ^ position_to_string start end_) - ~input ~start_pos:start.pos_cnum ~end_pos:end_.pos_cnum - in - Query_error.format ~colorize err - let parse ~debug ~colorize input = let buf = Sedlexing.Utf8.from_string input in let next_token () = provider ~debug buf in @@ -57,13 +54,7 @@ let parse ~debug ~colorize input = ~end_pos:!last_position.loc_end.pos_cnum in Error (Query_error.format ~colorize err) - | exception Parse_errors.Parse_error (msg, start, end_) -> - let err = - Query_error.parse_error ~message:msg ~input ~start_pos:start.pos_cnum - ~end_pos:end_.pos_cnum - in - Error (Query_error.format ~colorize err) - | exception Parse_errors.Rich_parse_error (err, start, end_) -> + | exception Query_error.Parse_error (err, start, end_) -> let err = Query_error.with_location ~input ~start_pos:start.pos_cnum ~end_pos:end_.pos_cnum err @@ -71,7 +62,12 @@ let parse ~debug ~colorize input = Error (Query_error.format ~colorize err) | exception _exn -> let Location.{ loc_start; loc_end; _ } = !last_position in - Error (pretty_print_error ~colorize ~input ~start:loc_start ~end_:loc_end) + let err = + Query_error.parse_error + ~message:("problem parsing at " ^ position_to_string loc_start loc_end) + ~input ~start_pos:loc_start.pos_cnum ~end_pos:loc_end.pos_cnum + in + Error (Query_error.format ~colorize err) let run ?(debug = false) ?(colorize = true) ?(verbose = false) ?(raw = false) ?(summarize = false) query json = diff --git a/source/Interpreter.ml b/source/Interpreter.ml index 81a22a4..4711638 100644 --- a/source/Interpreter.ml +++ b/source/Interpreter.ml @@ -146,10 +146,12 @@ end = struct let custom ~kind ~value message = fail ~kind:(Custom kind) ~value message end -type _ Effect.t += Yield : Json.t -> unit Effect.t -type _ Effect.t += Break : unit Effect.t -type _ Effect.t += Halt : int -> unit Effect.t -type _ Effect.t += User_error : Json.t -> unit Effect.t +type _ Effect.t += + | Yield : Json.t -> unit Effect.t + | Break : unit Effect.t + | Halt : int -> unit Effect.t + | User_error : Json.t -> unit Effect.t + type fn_definition = { params : string list; body : expression } type ctx = { @@ -170,60 +172,31 @@ let user_error value = let yield_many items = List.iter yield items let run fn ?and_then ?on_fail () = - let handler : 'a Effect.Deep.effect_handler = - { - effc = - (fun (type a) (eff : a Effect.t) -> - match eff with - | Yield json -> ( - match and_then with - | Some f -> - Some - (fun (k : (a, _) Effect.Deep.continuation) -> - f json; - Effect.Deep.continue k ()) - | None -> None) - | Runtime_error.Fail err -> ( - match on_fail with - | Some f -> - Some (fun (_ : (a, _) Effect.Deep.continuation) -> f err) - | None -> None) - | _ -> None); - } + let fn = + match and_then with + | None -> fn + | Some f -> ( + fun () -> + match fn () with + | () -> () + | effect Yield json, k -> + f json; + Effect.Deep.continue k ()) in - Effect.Deep.try_with fn () handler + match on_fail with + | None -> fn () + | Some f -> ( + match fn () with () -> () | effect Runtime_error.Fail err, _ -> f err) let run_while fn ~when_ = - let handler : 'a Effect.Deep.effect_handler = - { - effc = - (fun (type a) (eff : a Effect.t) -> - match eff with - | Yield v -> - Some - (fun (k : (a, _) Effect.Deep.continuation) -> - if when_ v then Effect.Deep.continue k () else ()) - | _ -> None); - } - in - Effect.Deep.try_with fn () handler + match fn () with + | () -> () + | effect Yield v, k -> if when_ v then Effect.Deep.continue k () else () let run_and_collect_results fn = - let handler : (unit, Json.t list) Effect.Deep.handler = - { - retc = (fun () -> []); - exnc = (fun e -> raise e); - effc = - (fun (type a) (eff : a Effect.t) -> - match eff with - | Yield v -> - Some - (fun (k : (a, _) Effect.Deep.continuation) -> - v :: Effect.Deep.continue k ()) - | _ -> None); - } - in - Effect.Deep.match_with fn () handler + match fn () with + | () -> [] + | effect Yield v, k -> v :: Effect.Deep.continue k () let rec substitute_params (params : string list) (args : expression list) (expr : expression) : expression = @@ -3478,24 +3451,13 @@ let execute ~colorize ~verbose ?(env = []) expr json = in Query_error.format ~colorize qerr in - let handler : ('a, ('a, string) result) Effect.Deep.handler = - { - retc = (fun results -> Ok results); - exnc = (fun e -> Error (Printexc.to_string e)); - effc = - (fun (type a) (eff : a Effect.t) -> - match eff with - | Runtime_error.Fail err -> Some (fun _ -> Error (format_error err)) - | Break -> - Some - (fun (_ : (a, _) Effect.Deep.continuation) -> - let err = - Query_error.context_error - ~message:"break used outside of loop context" - in - Error (Query_error.format ~colorize err)) - | Halt exit_code -> Some (fun _ -> exit exit_code) - | _ -> None); - } - in - Effect.Deep.match_with (fun () -> collect ~ctx expr json) () handler + match collect ~ctx expr json with + | results -> Ok results + | effect Runtime_error.Fail err, _ -> Error (format_error err) + | effect Break, _ -> + let err = + Query_error.context_error ~message:"break used outside of loop context" + in + Error (Query_error.format ~colorize err) + | effect Halt exit_code, _ -> exit exit_code + | exception e -> Error (Printexc.to_string e) diff --git a/source/Language.ml b/source/Language.ml index bc91d9c..ba9cafe 100644 --- a/source/Language.ml +++ b/source/Language.ml @@ -1305,56 +1305,6 @@ let all_function_names () = let aliases = List.concat_map (fun (f : function_info) -> f.aliases) funcs in List.sort_uniq Stdlib.String.compare (names @ aliases) -(* Levenshtein distance for fuzzy matching *) -let levenshtein s1 s2 = - let len1 = String.length s1 in - let len2 = String.length s2 in - if len1 = 0 then len2 - else if len2 = 0 then len1 - else - let matrix = Array.make_matrix (len1 + 1) (len2 + 1) 0 in - for i = 0 to len1 do - matrix.(i).(0) <- i - done; - for j = 0 to len2 do - matrix.(0).(j) <- j - done; - for i = 1 to len1 do - for j = 1 to len2 do - let cost = if s1.[i - 1] = s2.[j - 1] then 0 else 1 in - matrix.(i).(j) <- - min - (min (matrix.(i - 1).(j) + 1) (matrix.(i).(j - 1) + 1)) - (matrix.(i - 1).(j - 1) + cost) - done - done; - matrix.(len1).(len2) - -(* Find similar function names for suggestions *) -let suggest_function_name (typo : string) : string list = - let all_names = all_function_names () in - let typo_lower = String.lowercase_ascii typo in - let typo_len = String.length typo in - (* Calculate distance and filter candidates *) - let candidates = - all_names - |> List.map (fun name -> - let name_lower = String.lowercase_ascii name in - let dist = levenshtein typo_lower name_lower in - (name, dist)) - |> List.filter (fun (name, dist) -> - (* Accept if distance is reasonable relative to length *) - let name_len = String.length name in - let max_dist = max 2 (min typo_len name_len / 3) in - dist <= max_dist) - |> List.sort (fun (_, d1) (_, d2) -> compare d1 d2) - |> List.map fst - in - (* Return top 3 suggestions *) - match candidates with - | [] -> [] - | _ -> List.filteri (fun i _ -> i < 3) candidates - let type_name_of_applicable = function | String -> "string" | Array -> "array" @@ -1364,15 +1314,6 @@ let type_name_of_applicable = function | Nil -> "null" | Any -> "any" -let applicable_of_type_name = function - | "string" -> Some String - | "array" -> Some Array - | "object" -> Some Object - | "number" -> Some Number - | "boolean" -> Some Bool - | "null" -> Some Nil - | _ -> None - let functions_for_type type_name = let all = all_functions () in List.filter @@ -1382,9 +1323,6 @@ let functions_for_type type_name = f.applicable_to) all -let function_names_for_type type_name = - functions_for_type type_name |> List.map (fun (f : function_info) -> f.name) - let applicable_of_json_type = function | "string" -> String | "array" -> Array diff --git a/source/Language.mli b/source/Language.mli index c10e313..e478ff2 100644 --- a/source/Language.mli +++ b/source/Language.mli @@ -28,11 +28,8 @@ val find_category : string -> category option val category_names : unit -> string list val all_functions : unit -> function_info list val all_function_names : unit -> string list -val suggest_function_name : string -> string list val functions_for_type : string -> function_info list -val function_names_for_type : string -> string list val type_name_of_applicable : applicable_to -> string -val applicable_of_type_name : string -> applicable_to option val applicable_of_json_type : string -> applicable_to val find_function : string -> function_info option val error_for_missing_arg : string -> Query_error.t diff --git a/source/Lexer.ml b/source/Lexer.ml index 427b233..75a65a2 100644 --- a/source/Lexer.ml +++ b/source/Lexer.ml @@ -8,7 +8,6 @@ let space = [%sedlex.regexp? Plus ('\n' | '\t' | ' ')] let identifier = [%sedlex.regexp? (alphabetic | '_'), Star (alphabetic | digit | '_')] -let not_double_quotes = [%sedlex.regexp? Compl '"'] let comment = [%sedlex.regexp? '#', Star (Compl '\n')] (* Custom pp for Z.t since ppx_deriving can't derive it *) diff --git a/source/Parse_errors.ml b/source/Parse_errors.ml deleted file mode 100644 index 849a03c..0000000 --- a/source/Parse_errors.ml +++ /dev/null @@ -1,8 +0,0 @@ -exception Parse_error of string * Lexing.position * Lexing.position -exception Rich_parse_error of Query_error.t * Lexing.position * Lexing.position - -let raise_error msg start_pos end_pos = - raise (Parse_error (msg, start_pos, end_pos)) - -let raise_rich_error err start_pos end_pos = - raise (Rich_parse_error (err, start_pos, end_pos)) diff --git a/source/Parser.mly b/source/Parser.mly index 3ebf77d..d9b6c96 100644 --- a/source/Parser.mly +++ b/source/Parser.mly @@ -320,7 +320,7 @@ term: | f = FUNCTION; arg1 = sequence_expr; SEMICOLON; arg2 = sequence_expr; CLOSE_PARENT; opt = boption(QUESTION_MARK) { let ast = match Language.map_binary_fn f arg1 arg2 with | Ok ast -> ast - | Error err -> Parse_errors.raise_rich_error err $startpos(arg1) $endpos(arg2) + | Error err -> Query_error.raise err $startpos(f) $endpos(f) in match opt with | true -> Optional ast @@ -332,9 +332,9 @@ term: if Language.can_default_to_identity f then match Language.map_unary_fn f Identity with | Ok ast -> ast - | Error err -> Parse_errors.raise_rich_error err $startpos(f) $endpos(f) + | Error err -> Query_error.raise err $startpos(f) $endpos(f) else - Parse_errors.raise_rich_error (Language.error_for_missing_arg f) $startpos(f) $endpos(f) + Query_error.raise (Language.error_for_missing_arg f) $startpos(f) $endpos(f) in match opt with | true -> Optional ast @@ -343,7 +343,7 @@ term: | f = FUNCTION; arg = sequence_expr; CLOSE_PARENT; opt = boption(QUESTION_MARK) { let ast = match Language.map_unary_fn f arg with | Ok ast -> ast - | Error err -> Parse_errors.raise_rich_error err $startpos(arg) $endpos(arg) + | Error err -> Query_error.raise err $startpos(f) $endpos(f) in match opt with | true -> Optional ast @@ -360,7 +360,7 @@ term: | f = IDENTIFIER; opt = boption(QUESTION_MARK) { let ast = match Language.map_nullary_fn f with | Ok ast -> ast - | Error err -> Parse_errors.raise_rich_error err $startpos(f) $endpos(f) + | Error err -> Query_error.raise err $startpos(f) $endpos(f) in match opt with | true -> Optional ast @@ -370,7 +370,7 @@ term: | f = IDENTIFIER; QUESTION_MARK; OPEN_PARENT; arg = sequence_expr; CLOSE_PARENT { let ast = match Language.map_unary_fn f arg with | Ok ast -> ast - | Error err -> Parse_errors.raise_rich_error err $startpos(arg) $endpos(arg) + | Error err -> Query_error.raise err $startpos(f) $endpos(f) in Optional ast } @@ -378,7 +378,7 @@ term: | f = IDENTIFIER; QUESTION_MARK; OPEN_PARENT; arg1 = sequence_expr; SEMICOLON; arg2 = sequence_expr; CLOSE_PARENT { let ast = match Language.map_binary_fn f arg1 arg2 with | Ok ast -> ast - | Error err -> Parse_errors.raise_rich_error err $startpos(arg1) $endpos(arg2) + | Error err -> Query_error.raise err $startpos(f) $endpos(f) in Optional ast } diff --git a/source/Query_error.ml b/source/Query_error.ml index 328b1ef..85da11c 100644 --- a/source/Query_error.ml +++ b/source/Query_error.ml @@ -1,8 +1,4 @@ -type location = { - input : string; (* the full query string *) - start_pos : int; (* character offset *) - end_pos : int; (* character offset *) -} +type location = { input : string; start_pos : int; end_pos : int } type context = | Json_value of Json.t @@ -29,7 +25,6 @@ let with_location ~input ~start_pos ~end_pos err = { err with location = Some { input; start_pos; end_pos } } let with_context ctx err = { err with contexts = ctx :: err.contexts } -let with_suggestion s err = { err with suggestion = Some s } let format_location ~colorize loc = let t = Console_style.make ~colorize in @@ -169,22 +164,6 @@ let missing_argument ~fn_name ?message ~usage ~description ?applicable_to in make ~kind:"missing_argument" ~message:msg ~contexts () -let empty_collection ~operation = - make ~kind:"empty_collection" - ~message:(Printf.sprintf "cannot apply `%s` to empty collection" operation) - () - -let null_access ~key = - make ~kind:"null_access" - ~message:(Printf.sprintf "cannot access key `%s` on null" key) - ~suggestion:"check if value exists before accessing" () - -let index_out_of_bounds ~index ~length = - make ~kind:"index_out_of_bounds" - ~message: - (Printf.sprintf "index `%d` out of bounds (length: %d)" index length) - ~suggestion:"use `.[index]?` for optional access" () - let parse_error ~message ~input ~start_pos ~end_pos = make ~kind:"parse_error" ~message ~location:{ input; start_pos; end_pos } () @@ -207,3 +186,7 @@ let runtime_error ~kind ~message ?value ?suggestion ?expected ?found () = match found with Some f -> with_context (Found f) err | None -> err let context_error ~message = make ~kind:"context_error" ~message () + +exception Parse_error of t * Lexing.position * Lexing.position + +let raise err start_pos end_pos = raise (Parse_error (err, start_pos, end_pos)) diff --git a/source/Query_error.mli b/source/Query_error.mli index a0ce9e1..8d95e94 100644 --- a/source/Query_error.mli +++ b/source/Query_error.mli @@ -20,7 +20,6 @@ type t = { val with_location : input:string -> start_pos:int -> end_pos:int -> t -> t val with_context : context -> t -> t -val with_suggestion : string -> t -> t val format : colorize:bool -> t -> string (** Format error with colors and Rust-style layout *) @@ -64,10 +63,6 @@ val missing_argument : unit -> t -val empty_collection : operation:string -> t -val null_access : key:string -> t -val index_out_of_bounds : index:int -> length:int -> t - val parse_error : message:string -> input:string -> start_pos:int -> end_pos:int -> t @@ -88,3 +83,7 @@ val runtime_error : t val context_error : message:string -> t + +exception Parse_error of t * Lexing.position * Lexing.position + +val raise : t -> Lexing.position -> Lexing.position -> 'a diff --git a/source/console-style/Console_style.ml b/source/console-style/Console_style.ml index 9e32dd4..99e1621 100644 --- a/source/console-style/Console_style.ml +++ b/source/console-style/Console_style.ml @@ -2,7 +2,6 @@ open Ansi let indent n = String.make (n * 1) ' ' let enter n = String.make n '\n' -let double_quotes str = "\"" ^ str ^ "\"" let single_quotes str = "'" ^ str ^ "'" type t = { @@ -31,30 +30,6 @@ let make ~colorize = styled; } -let bold ~colorize text = - if colorize then Ansi.styled ~reset:true ~bold:true text else text - -let red ~colorize text = - if colorize then Ansi.styled ~reset:true ~fg:Color.red text else text - -let green ~colorize text = - if colorize then Ansi.styled ~reset:true ~fg:Color.green text else text - -let yellow ~colorize text = - if colorize then Ansi.styled ~reset:true ~fg:Color.yellow text else text - -let blue ~colorize text = - if colorize then Ansi.styled ~reset:true ~fg:Color.blue text else text - -let gray ~colorize text = - if colorize then Ansi.styled ~reset:true ~fg:Color.bright_black text else text - -let cyan ~colorize text = - if colorize then Ansi.styled ~reset:true ~fg:Color.cyan text else text - -let styled ~colorize ?fg ?bold text = - if colorize then Ansi.styled ~reset:true ?fg ?bold text else text - module Buffer = struct let green ~colorize buf = if colorize then Stdlib.Buffer.add_string buf "\027[32m" diff --git a/source/jotason/Common.ml b/source/jotason/Common.ml index 989d075..176934a 100644 --- a/source/jotason/Common.ml +++ b/source/jotason/Common.ml @@ -29,11 +29,6 @@ let init_lexer ?buf ?fname ?(lnum = 1) () = let buf = match buf with None -> Buffer.create 256 | Some buf -> buf in { buf; lnum; bol = 0; fname } -(** Check if a JSON value is truthy (jq semantics: false and null are falsy, - everything else is truthy) *) -let is_truthy (json : t) : bool = - match json with `Bool false | `Null -> false | _ -> true - (** Get numeric value as float for comparison, None for non-numeric types *) let to_float (json : t) : float option = match json with diff --git a/website/App.mlx b/website/App.mlx index ad8b1ba..f14d863 100644 --- a/website/App.mlx +++ b/website/App.mlx @@ -35,9 +35,12 @@ let initial_json = } |} -type page = Playground | FunctionsPage +type page = Playground | Functions -type t = { +let path_of_page = function Playground -> "/" | Functions -> "/functions" +let navigate_to page = Router.navigate (path_of_page page) + +type state = { query : string; json : string option; copied : bool; @@ -66,9 +69,9 @@ module Query_params = struct let decode json = of_json json let encode t = to_json t - let toString t = t |> encode |> Melange_json.to_string - let fromString str = str |> Melange_json.of_string |> decode - let toHash state = state |> toString |> Base64.encode + let to_string t = t |> encode |> Melange_json.to_string + let from_string str = str |> Melange_json.of_string |> decode + let to_hash state = state |> to_string |> Base64.encode end module Option = struct @@ -90,10 +93,6 @@ module Query_json = struct | 1 -> Error res.second | _ -> Error "Invalid result from QueryJsonJs" - let is_loaded : unit -> bool = - [%mel.raw - {| function () { return typeof window['query-json'] !== 'undefined' && typeof window['query-json'].run === 'function'; }|}] - let query_json_run : string -> string -> jsoo_result = [%mel.raw {| function (query, json) { return window['query-json'].run(query, json); }|}] @@ -106,43 +105,34 @@ module Clipboard = struct [@@mel.scope "navigator", "clipboard"] end -module PlaygroundPage = struct - let[@react.component] make ~state ~dispatch ~is_ready ~on_navigate_functions = - let on_query_change = fun value -> dispatch (Update_query value) in - let on_json_change = fun value -> dispatch (Update_json value) in - +module Playground_page = struct + let[@react.component] make ~state ~update_query ~update_json ~set_copied + ~set_modal_open ~on_navigate_functions = let output = - React.useMemo3 + React.useMemo2 (fun () -> - if not is_ready then None - else - match (state.json, state.query) with - | Some _, "" | None, _ -> None - | Some json, _ -> Some (Query_json.run state.query json)) - (state.json, state.query, is_ready) + match (state.json, state.query) with + | Some _, "" | None, _ -> None + | Some json, _ -> Some (Query_json.run state.query json)) + (state.json, state.query) in let on_share_click = React.useCallback2 (fun _ -> let hash = - Query_params.toHash { query = state.query; json = state.json } + Query_params.to_hash { query = state.query; json = state.json } in match hash with | Some hash -> - let url = - Web.window |> Web.Window.location |> Web.Location.origin - in - let fullUrl = url ^ "#" ^ hash in + let fullUrl = Router.get_origin () ^ "#" ^ hash in Router.set_hash hash; let _promise = Clipboard.write_text fullUrl |> Js.Promise.then_ (fun () -> - dispatch (Set_copied true); + set_copied true; let _timeout = - Js.Global.setTimeout - ~f:(fun () -> dispatch (Set_copied false)) - 2000 + Js.Global.setTimeout ~f:(fun () -> set_copied false) 2000 in Js.Promise.resolve ()) |> Js.Promise.catch (fun _err -> @@ -154,12 +144,12 @@ module PlaygroundPage = struct (state.query, state.json) in - let on_examples_click = fun _ -> dispatch (Set_modal_open true) in - let on_close_modal = fun () -> dispatch (Set_modal_open false) in + let on_examples_click = fun _ -> set_modal_open true in + let on_close_modal = fun () -> set_modal_open false in let on_select_example = fun query json -> - dispatch (Update_query query); - dispatch (Update_json json) + update_query query; + update_json json in