Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,40 @@ rust/target/
.eslintcache
.detekt/


# Clojure
clojure/.cpcache/
clojure/.clj-kondo/
clojure/classes/
clojure/target/

# OCaml
ocaml/run_tests
ocaml/**/*.cmi
ocaml/**/*.cmo
ocaml/**/*.cmx
ocaml/**/*.o

# Scala
scala/out/
scala/out-lint/
scala/**/*.tasty

# Dart
dart/.dart_tool/
dart/.packages
dart/pubspec.lock
dart/*.dill

# Elixir
elixir/_build/
elixir/.elixir_ls/
elixir/deps/
elixir/**/*.beam
elixir/erl_crash.dump

# Haskell
haskell/.hsbuild/
haskell/**/*.hi
haskell/**/*.o
haskell/dist-newstyle/
14 changes: 10 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ matrix in [`REPORT.md`](design/REPORT.md)):

| Canonical | Complete | Partial |
|---|---|---|
| typescript | javascript, python, go, php, ruby, lua, rust, c, csharp, zig, cpp, perl, swift | java, kotlin |
| typescript | javascript, python, go, php, ruby, lua, rust, c, csharp, zig, cpp, perl, swift, clojure, ocaml, scala, java, kotlin, dart, elixir, haskell | — |


## Prime directives (do not break these)
Expand Down Expand Up @@ -128,10 +128,16 @@ into the directory and use its `Makefile`. First run installs deps.
| C++ | `cpp/` | `make test` (needs `nlohmann/json` header) | clang-tidy + clang-format | `_v`/`_str` suffix variants |
| C# | `csharp/` | `dotnet test` | Roslyn analyzers | PascalCase; SDK pinned to 8.0 on purpose |
| Zig | `zig/` | `zig build test` | `zig build` + `zig fmt` | `allocator` is the first parameter |
| Java | `java/` | `mvn test` | checkstyle + spotbugs | lowercase names; partial port (JUnit 6) |
| Kotlin | `kotlin/` | `./gradlew test` | detekt + ktlint | partial port |
| Java | `java/` | `mvn test` | checkstyle + spotbugs | lowercase names; JUnit 6 |
| Kotlin | `kotlin/` | `./gradlew test` | detekt + ktlint | |
| Perl | `perl/` | `prove -Ilib t/` | perlcritic | `Tie::IxHash`-style ordered hash |
| Swift | `swift/` | `swift test` | swift-format | `allocator`-free; in-tree ordered dict |
| Clojure | `clojure/` | `clojure -M:test` | namespace compile check | mutable `LinkedHashMap`/`ArrayList` nodes; lower-smushed names |
| OCaml | `ocaml/` | `make test` (`ocamlc`) | type-check (`ocamlc -c`) | `value` variant; distinct Noval/Null (like TS); in-tree regex engine |
| Scala | `scala/` | `make test` (`scalac`/`scala`) | type-check (`scalac`) | `Value` ADT; distinct Noval/VNull (like TS); `java.util.regex` |
| Dart | `dart/` | `dart run test/runner.dart` | `dart analyze` | native `Map`/`List` nodes; single `null` (like Python); core `RegExp` |
| Elixir | `elixir/` | `elixir test/runner.exs` | compile check (`elixirc`) | ETS-backed heap nodes (`{:vmap,_}`/`{:vlist,_}`); single `nil` (like Python); core `Regex` |
| Haskell | `haskell/` | `ghc … test/Runner.hs` | type-check (`ghc -fno-code`) | `IORef`-backed nodes (whole API in `IO`); distinct `VNoval`/`VNull` (like OCaml); in-tree Vregex |

Repo-wide: `make test` / `make lint` / `make audit` (supply-chain) /
`make scan` (secrets, SAST, parity, regex, spelling, markdown) /
Expand Down Expand Up @@ -166,7 +172,7 @@ markdownlint, plus each language's linters).

## Conventions

- **Casing.** `getpath` (TS/JS/Py/Ruby/PHP/Lua/Perl/Java/Kotlin/Swift),
- **Casing.** `getpath` (TS/JS/Py/Ruby/PHP/Lua/Perl/Java/Kotlin/Swift/Clojure/OCaml/Scala/Dart/Elixir/Haskell),
`GetPath` (Go/C#), `get_path` (Rust), `voxgig_getpath` (C — and C++ adds
`_v`/`_str` variants). Parity is checked case/underscore-insensitively.
- **Absent vs. null ("Group A/B").** See [`UNDEF_SPEC.md`](design/UNDEF_SPEC.md).
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# `make -C <dir>`. Each port ships at least `test` and `lint`; `build`,
# `inspect`, `clean` and `reset` are invoked tolerantly (a port without one
# just reports "(no <t> target)").
LANGS = typescript javascript python go ruby php lua zig java rust c cpp csharp kotlin perl swift
LANGS = typescript javascript python go ruby php lua zig java rust c cpp csharp kotlin perl swift clojure ocaml scala dart elixir haskell

# Every port ships a `make lint` target, so lint covers the full set.
LINT_LANGS = $(LANGS)
Expand All @@ -31,7 +31,7 @@ AUDIT_LANGS = typescript javascript python go ruby php rust csharp
# LuaRocks, Maven Central, CPAN) and ALWAYS creates + pushes a git tag
# `<lang>/vX.Y.Z`. Registry-less ports (Go, PHP/Packagist, Swift, Zig, C, C++)
# publish purely by that tag.
PUBLISH_LANGS = typescript javascript python go ruby php lua zig java rust c cpp csharp kotlin perl swift
PUBLISH_LANGS = typescript javascript python go ruby php lua zig java rust c cpp csharp kotlin perl swift clojure ocaml scala dart elixir haskell

.PHONY: all inspect build test lint audit scan analyze clean reset publish status corpus gen-docs \
scan-secrets scan-deps scan-sast scan-workflows scan-shell scan-spelling scan-docs \
Expand Down
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,20 @@ syntax), and language-specific notes:
| C | Complete | [`c/README.md`](./c/README.md) |
| C# | Complete | [`csharp/README.md`](./csharp/README.md) |
| Zig | Complete | [`zig/README.md`](./zig/README.md) |
| Java | Partial | [`java/README.md`](./java/README.md) |
| Kotlin | Partial | [`kotlin/README.md`](./kotlin/README.md) |
| Java | Complete | [`java/README.md`](./java/README.md) |
| Kotlin | Complete | [`kotlin/README.md`](./kotlin/README.md) |
| C++ | Complete | [`cpp/README.md`](./cpp/README.md) |
| Perl | Complete | [`perl/README.md`](./perl/README.md) |
| Swift | Complete | [`swift/README.md`](./swift/README.md) |

"Partial" (Java, Kotlin) denotes project maturity / release-lag — the
JVM family trails the canonical by a release — **not** missing API: both
report full canonical parity under `tools/check_parity.py`. See each
| Clojure | Complete | [`clojure/README.md`](./clojure/README.md) |
| OCaml | Complete | [`ocaml/README.md`](./ocaml/README.md) |
| Scala | Complete | [`scala/README.md`](./scala/README.md) |
| Dart | Complete | [`dart/README.md`](./dart/README.md) |
| Elixir | Complete | [`elixir/README.md`](./elixir/README.md) |
| Haskell | Complete | [`haskell/README.md`](./haskell/README.md) |

Every listed port reports full canonical parity under
`tools/check_parity.py` and passes the shared `build/test/` corpus. See each
port's `README.md` for details.

Each port directory also carries a `DOCS.md` (the comprehensive,
Expand Down Expand Up @@ -429,8 +434,8 @@ cross-engine edge cases:
├── build/test/ # shared JSON test corpus (.jsonic)
├── typescript/ javascript/ python/ # canonical + JS-family ports
├── go/ ruby/ php/ # other complete ports
├── lua/ csharp/ zig/ rust/ c/ perl/ kotlin/ cpp/ swift/
├── java/ # partial port
├── lua/ csharp/ zig/ rust/ c/ perl/ kotlin/ cpp/ swift/ clojure/ ocaml/ scala/ dart/ elixir/ haskell/
├── java/ # JVM ports (also kotlin/ scala/ clojure/)
└── LICENSE
```

Expand Down Expand Up @@ -460,6 +465,12 @@ Each language directory contains:
| Zig | `zig build` (compiler) | `zig fmt` |
| C# | Roslyn analyzers | `dotnet format` |
| Kotlin | detekt | ktlint |
| Clojure | namespace compile check | (clj-kondo optional) |
| OCaml | type-check (`ocamlc -c`) | (ocamlformat optional)|
| Scala | type-check (`scalac`) | (scalafmt optional) |
| Dart | `dart analyze` | `dart format` |
| Elixir | compile check (`elixirc`) | (`mix format` optional)|
| Haskell | type-check (`ghc -fno-code`) | (`ormolu` optional) |

Run everything with `make lint` at the repo root, or one language with
`make lint-<lang>` (e.g. `make lint-go`).
Expand Down
73 changes: 73 additions & 0 deletions clojure/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# AGENTS.md — Clojure port of `voxgig/struct`

Read the repo-root [`AGENTS.md`](../AGENTS.md) first. This file covers only
what is specific to the Clojure port. **TypeScript is canonical; the shared
`build/test/*.jsonic` corpus is the contract.** This port is a faithful
translation of the canonical implementation (modelled most closely on the
Python port, which shares Clojure's single-`nil` world).

## How to build / test / lint

```
cd clojure
make test # clojure -M:test — runs build/test/test.json through the port
make lint # compiles the library + runner namespaces (a clean load = pass)
```

Requires the Clojure CLI (`clojure`/`clj`) and a JDK on `PATH`. The library
itself has **zero third-party runtime dependencies**; the test runner reads
the corpus with a small in-tree JSON reader (no JSON library).

## The one thing to understand: nodes are mutable Java collections

The canonical algorithm assumes nodes are **mutable and reference-stable**:
`walk`, `merge`, `inject`, `transform`, `validate` and the `Injection`
state machine mutate nodes in place and rely on shared references. Idiomatic
immutable Clojure maps/vectors cannot model that without rewriting the
algorithm, which would break uniformity. So this port represents nodes with:

- **maps → `java.util.LinkedHashMap`** (insertion-ordered, like a JS object),
- **lists → `java.util.ArrayList`** (mutable, reference-stable).

`ismap`/`islist`/`isnode` test `java.util.Map`/`java.util.List`. All node-
creating code (`{}`/`[]` in the canonical) builds `LinkedHashMap`/`ArrayList`
via the private `lhm`/`alist` helpers. **Never** introduce a persistent
Clojure map/vector as a *node* — only as a short-lived read-only intermediate.

## `nil` is both `undefined` and JSON `null`

Like Python, Clojure has only `nil`. The canonical `undefined` (absent) and
JSON `null` both map to `nil`. The Group A/B rules (see
[`design/UNDEF_SPEC.md`](../design/UNDEF_SPEC.md)) recover the distinction
where it matters:

- Group A readers (`getprop`, `getelem`, `haskey`, `isnode`, `isempty`)
treat a stored `nil` as "no value".
- Group B processors (`setprop`, `clone`, `merge`, `walk`, `inject`, …)
preserve `nil` literally. `_lookup` is the internal raw reader.

A few functions distinguish "no argument supplied" from `nil` via the public
`NOARG` sentinel (mirrors Python's `_ABSENT`): `typify` (→ `T_noval` vs
`T_null`), `stringify` (→ `""` vs `"null"`), `pathify`.

## Naming

Public function names are **lower-smushed, exactly the canonical names**
(`getpath`, `getprop`, `ismap`, `isnode`, `setpath`, `checkPlacement`,
`re_find_all`, …) so the case/underscore-insensitive parity check in
`tools/check_parity.py` matches them directly. The namespace `:refer-clojure
:exclude`s `merge`, `filter`, `flatten` and `replace` to reuse those names.

## Gotchas

- **Identity markers.** `SKIP` and `DELETE` are specific `LinkedHashMap`
instances; compare with `identical?` (never `=`).
- **The `Injection` is a distinct type** (`deftype Inj` over a mutable
`HashMap`), so it is never mistaken for a data map by `ismap`. Access its
fields only through the internal `ig`/`is!` helpers.
- **Numbers.** JSON integers parse to `Long`, decimals to `Double`. `typify`
splits integer/decimal on that; `stringify`/`jsonify` follow JS number
formatting (an integral `Double` prints without `.0`).
- **Keep `make test` and `python3 tools/check_parity.py` green**, and add no
runtime dependencies. If you change canonical behaviour, change the
TypeScript + corpus first, then propagate here.
114 changes: 114 additions & 0 deletions clojure/DOCS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Clojure port — comprehensive guide

This document covers the Clojure-specific details of `voxgig/struct`. For the
language-neutral concepts, tutorial and full reference, read the top-level
[`DOCS.md`](../DOCS.md); for the user overview, [`README.md`](./README.md).
TypeScript is canonical and the shared `build/test` corpus is the contract.

## Installation

Add the Clojure source to your project (Clojars coordinates are published per
release) or depend on this directory directly via a local `deps.edn` `:paths`
entry. Then `(require '[voxgig.struct :as s])`.

Requirements: a JDK and the Clojure CLI. No third-party runtime dependencies.

## Representation of data

| JSON-shape thing | Clojure representation |
|-------------------------|----------------------------------------------|
| object / map | `java.util.LinkedHashMap` (insertion order) |
| array / list | `java.util.ArrayList` |
| string | `java.lang.String` |
| integer | `java.lang.Long` |
| decimal | `java.lang.Double` |
| boolean | `java.lang.Boolean` |
| JSON `null` / undefined | `nil` |
| function (commands) | a Clojure fn (`fn?`) |

Nodes are **mutable and reference-stable** on purpose: the canonical
algorithms (`merge`, `walk`, `inject`, `transform`, `validate`) mutate nodes
in place and depend on shared references. Build nodes with `LinkedHashMap` /
`ArrayList` (or the `jm` / `jt` helpers); do not hand the library a persistent
Clojure map or vector as a node.

### `nil`: undefined vs JSON null

Clojure has a single `nil`, used for both the canonical `undefined` and JSON
`null`. The library follows the Group A / Group B rules
([`design/UNDEF_SPEC.md`](../design/UNDEF_SPEC.md)):

- **Group A** readers — `getprop`, `getelem`, `haskey`, `isnode`, `isempty` —
treat a stored `nil` as "no value" (it yields the default / `false`).
- **Group B** processors — `setprop`, `clone`, `merge`, `walk`, `inject`,
`transform`, `validate`, `select` — preserve `nil` literally.

Where a function must tell "no argument" from an explicit `nil`, pass the
public `NOARG` sentinel (this mirrors the absent/undefined case):

```clojure
(s/typify) ;=> T_noval (no argument = undefined)
(s/typify nil) ;=> T_scalar|T_null
(s/stringify) ;=> "" (undefined)
(s/stringify nil) ;=> "null" (JSON null)
(s/pathify s/NOARG) ;=> "<unknown-path>"
```

## The public API

Names are lower-smushed, identical to the canonical export list:

- **Lookups / paths:** `getpath`, `setpath`, `getprop`, `setprop`, `getelem`,
`delprop`, `haskey`, `keysof`, `items`.
- **Predicates / kinds:** `isnode`, `ismap`, `islist`, `iskey`, `isfunc`,
`isempty`, `typify`, `typename`.
- **Values:** `clone`, `merge`, `walk`, `size`, `slice`, `pad`, `flatten`,
`filter`, `getdef`, `strkey`.
- **Strings / formatting:** `stringify`, `jsonify`, `pathify`, `join`,
`escre`, `escurl`.
- **Regex (RE2-subset uniform API):** `re_compile`, `re_find`, `re_find_all`,
`re_replace`, `re_test`, `re_escape`.
- **By-example engine:** `inject`, `transform`, `validate`, `select`, plus the
injector helpers `checkPlacement`, `injectorArgs`, `injectChild`.
- **Builders / markers:** `jm`, `jt`, `SKIP`, `DELETE`, and the `T_*` /
`M_*` constants.

`struct-utility` returns a map of every public function, mirroring the
`StructUtility` container in the other ports.

## Examples

```clojure
(require '[voxgig.struct :as s])

;; merge (later wins; the first node is modified in place)
(s/merge (s/jt (s/jm "a" 1) (s/jm "b" 2))) ;=> {a 1, b 2}

;; transform: spec mirrors the desired output, backticks pull from data
(s/transform (s/jm "name" "alice")
(s/jm "user" (s/jm "id" "`name`"))) ;=> {user {id alice}}

;; validate: plain values are typed defaults; `$STRING` etc. are commands
(s/validate (s/jm "a" "x") (s/jm "a" "`$STRING`")) ;=> {a x}

;; select: MongoDB-style query over children
(s/select (s/jt (s/jm "a" 1) (s/jm "a" 2))
(s/jm "a" (s/jm "`$GT`" 1))) ;=> ({a 2, $KEY 1})
```

## Testing

`make test` runs the entire shared corpus (`../build/test/test.json`) through
the port via an in-tree JSON reader and the same runner logic as every other
port. Keep it green, keep `python3 ../tools/check_parity.py` green, and add no
runtime dependencies.

## Implementation notes

- The injection state is a distinct `deftype Inj` (over a mutable `HashMap`)
so it is never confused with a data map.
- `SKIP` / `DELETE` are identity markers — compared with `identical?`.
- Numbers follow JS formatting in `stringify` / `jsonify` (an integral
`Double` prints without a trailing `.0`).
- The `voxgig.struct` namespace `:refer-clojure :exclude`s `merge`, `filter`,
`flatten` and `replace` to reuse those canonical names.
31 changes: 31 additions & 0 deletions clojure/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Makefile for the Clojure port of voxgig/struct.
# Requires the Clojure CLI (`clojure` / `clj`) and a JDK on PATH.

.PHONY: test lint build inspect clean reset publish

# Run the shared JSON corpus through the Clojure implementation.
test:
clojure -M:test

# "Lint": compile the library + runner namespaces. A clean load means the
# code is syntactically and structurally sound. (No third-party linter is
# required; clj-kondo can be added to the :lint alias if available.)
lint:
clojure -M:lint -e "(require 'voxgig.struct :reload) (require 'voxgig.struct-runner :reload) (println \"ok\")"

build:
clojure -M -e "(compile 'voxgig.struct)" 2>/dev/null || clojure -M -e "(require 'voxgig.struct)"

inspect:
@clojure --version 2>/dev/null || true
@java -version 2>&1 | head -1

clean:
rm -rf .cpcache classes target

reset: clean

# The library publishes to Clojars; this target creates the git tag. Set up
# Clojars credentials (CLOJARS_USERNAME / CLOJARS_PASSWORD) for a real deploy.
publish:
@echo "clojure: publish via Clojars (deps-deploy) + git tag clojure/vX.Y.Z"
Loading
Loading