Skip to content

Commit a5e7722

Browse files
committed
Port Sqids to pure CLJC and harden quality tooling
This replaces the prior sqids-java/sqids-javascript integration with a native Clojure/ClojureScript implementation while keeping the public API stable. The algorithm path is now shared in CLJC, supports large integer domains, and improves readability with clearer naming, structure, and inline docs. Internally, encode/decode now run through a total result-oriented core so validation and error behavior are explicit and testable across platforms. Throwing is constrained to the public edge functions affected by API contracts. Platform-specific behavior was pushed into platform namespaces to reduce reader-conditionals and duplicated logic. Tooling was modernized for a model public Clojure library: local wrappers for clj-kondo and cljstyle, stricter lint policy, markdownlint + prettier hooks, Babashka-based blocklist generation, and a simplified test entrypoint via Kaocha. Testing and CI were expanded with always-on coverage enforcement, machine-readable coverage outputs, JUnit XML reporting, and artifact uploads. Specs and generators were strengthened to exercise canonical/erroneous decode paths more directly and improve failure quality. Documentation and contributor guidance were updated throughout, including repository standards in AGENTS.md and clearer maintenance/release workflows.
1 parent eba1561 commit a5e7722

40 files changed

+3012
-538
lines changed

.clj-kondo/config.edn

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
{:linters
2+
{:aliased-namespace-symbol {:level :error}
3+
:aliased-namespace-var-usage {:level :error}
4+
:aliased-referred-var {:level :error}
5+
:case-quoted-test {:level :error}
6+
:case-symbol-test {:level :error}
7+
:clj-kondo-config {:level :error}
8+
:cond-else {:level :error}
9+
:condition-always-true {:level :error}
10+
:consistent-alias {:level :error
11+
:aliases {clojure.edn edn
12+
clojure.set set
13+
clojure.spec.alpha s
14+
clojure.spec.gen.alpha gen
15+
clojure.string str
16+
clojure.test t}}
17+
:def-fn {:level :error}
18+
:deprecated-namespace {:level :error}
19+
:deprecated-var {:level :error}
20+
:destructured-or-always-evaluates {:level :error}
21+
:destructured-or-binding-of-same-map {:level :error}
22+
:discouraged-java-method {:level :error}
23+
:discouraged-namespace {:level :error}
24+
:discouraged-tag {:level :error}
25+
:discouraged-var {:level :error}
26+
:do-template {:level :error}
27+
:docstring-leading-trailing-whitespace {:level :error}
28+
:docstring-no-summary {:level :error}
29+
:duplicate-key-args {:level :error}
30+
:duplicate-refer {:level :error}
31+
:earmuffed-var-not-dynamic {:level :error}
32+
:equals-expected-position {:level :error
33+
:position :first}
34+
:equals-false {:level :error}
35+
:equals-float {:level :error}
36+
:equals-nil {:level :error}
37+
:equals-true {:level :error}
38+
:if-nil-return {:level :error}
39+
:inline-def {:level :error}
40+
:is-message-not-string {:level :error}
41+
:keyword-binding {:level :error}
42+
:line-length {:level :error
43+
:max-line-length 120}
44+
:locking-suspicious-lock {:level :error}
45+
:loop-without-recur {:level :error}
46+
:main-without-gen-class {:level :error}
47+
:min-clj-kondo-version {:level :error}
48+
:minus-one {:level :error}
49+
:missing-body-in-when {:level :error}
50+
:missing-clause-in-try {:level :error}
51+
:missing-docstring {:level :error}
52+
:missing-else-branch {:level :error}
53+
:missing-protocol-method {:level :error}
54+
:missing-test-assertion {:level :error}
55+
:multiple-async-in-deftest {:level :error}
56+
:non-arg-vec-return-type-hint {:level :error}
57+
:not-empty? {:level :error}
58+
:plus-one {:level :error}
59+
:reduce-without-init {:level :error}
60+
:redundant-call {:level :error}
61+
:redundant-do {:level :error}
62+
:redundant-expression {:level :error}
63+
:redundant-fn-wrapper {:level :error}
64+
:redundant-format {:level :error}
65+
:redundant-ignore {:level :error}
66+
:redundant-let {:level :error}
67+
:redundant-let-binding {:level :error}
68+
:redundant-nested-call {:level :error}
69+
:redundant-primitive-coercion {:level :error}
70+
:redundant-str-call {:level :error}
71+
:refer {:level :error}
72+
:refer-all {:level :error}
73+
:redefined-var {:level :error}
74+
:schema-misplaced-return {:level :error}
75+
:self-requiring-namespace {:level :error}
76+
:shadowed-fn-param {:level :error}
77+
:shadowed-var {:level :error}
78+
:single-key-in {:level :error}
79+
:single-logical-operand {:level :error}
80+
:single-operand-comparison {:level :error}
81+
:underscore-in-namespace {:level :error}
82+
:unbound-destructuring-default {:level :error}
83+
:uninitialized-var {:level :error}
84+
:unreachable-code {:level :error}
85+
:unresolved-excluded-var {:level :error}
86+
:unresolved-namespace {:level :error}
87+
:unresolved-protocol-method {:level :error}
88+
:unresolved-var {:level :error}
89+
:unquote-not-syntax-quoted {:level :error}
90+
:unsorted-imports {:level :error}
91+
:unsorted-required-namespaces {:level :error
92+
:sort :case-insensitive}
93+
:unused-alias {:level :error}
94+
:unused-binding {:level :error
95+
:exclude-destructured-keys-in-fn-args false
96+
:exclude-destructured-as false
97+
:exclude-defmulti-args false}
98+
:unused-excluded-var {:level :error}
99+
:unused-import {:level :error}
100+
:unused-namespace {:level :error
101+
:simple-libspec false}
102+
:unused-private-var {:level :error}
103+
:unused-referred-var {:level :error}
104+
:unused-value {:level :error}
105+
:use {:level :error}
106+
:used-underscored-binding {:level :error}
107+
:var-same-name-except-case {:level :error}
108+
:warn-on-reflection {:level :error
109+
:warn-only-on-interop true}}}

.github/workflows/clojure.yml

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
name: Set up Java
2424
with:
2525
distribution: temurin
26-
java-version: 11
26+
java-version: 21
2727
- uses: actions/setup-python@v4
2828
name: Set up Python
2929
with:
@@ -34,10 +34,6 @@ jobs:
3434
with:
3535
path: ~/.cache/pre-commit
3636
key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
37-
- name: Docker cache
38-
uses: ScribeMD/docker-cache@0.3.6
39-
with:
40-
key: ${{ runner.os }}-docker-${{ hashFiles('.pre-commit-config.yaml') }}
4137
- uses: actions/cache@v3
4238
name: Clojure cache
4339
with:
@@ -47,19 +43,32 @@ jobs:
4743
~/.clojure
4844
~/.cpcache
4945
key: ${{ runner.os }}-clojure-${{ hashFiles('deps.edn') }}
50-
- name: Install Clojure
51-
run: |
52-
curl -L -O https://github.com/clojure/brew-install/releases/latest/download/posix-install.sh
53-
chmod +x posix-install.sh
54-
sudo ./posix-install.sh
55-
rm posix-install.sh
46+
- name: Install Clojure tools
47+
uses: DeLaGuardo/setup-clojure@13
48+
with:
49+
cli: latest
50+
bb: latest
51+
clj-kondo: 2026.01.19
52+
cljstyle: 0.15.0
5653
- name: Run pre-commit hooks
5754
run: |
5855
pip install -r requirements.txt
59-
pre-commit run --all-files
60-
- name: Run clj tests
61-
run: bin/test clj
62-
- name: Run cljs tests
56+
SKIP=kaocha-test pre-commit run --all-files
57+
- name: Run tests
6358
run: |
6459
npm install
65-
bin/test cljs
60+
bin/kaocha
61+
- name: Upload coverage artifact
62+
if: always()
63+
uses: actions/upload-artifact@v4
64+
with:
65+
name: coverage-report
66+
path: target/coverage
67+
if-no-files-found: warn
68+
- name: Upload JUnit XML artifact
69+
if: always()
70+
uses: actions/upload-artifact@v4
71+
with:
72+
name: junit-xml
73+
path: target/test-results/junit.xml
74+
if-no-files-found: warn

.gitignore

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
*.class *.iml
1+
*.class
2+
*.iml
23
*.jar
34
*.log
45
*.swp
@@ -7,13 +8,19 @@
78
.calva/output-window/
89
.classpath
910
.clj-kondo/.cache
11+
.cljs_node_repl/
1012
.cpcache
13+
.cpcache/
1114
.eastwood
1215
.factorypath
1316
.hg/
1417
.hgignore
1518
.idea
1619
.lein-*
20+
.lein-deps-sum
21+
.lein-failures
22+
.lein-plugins/
23+
.lein-repl-history
1724
.lsp/.cache
1825
.lsp/sqlite.db
1926
.nrepl-*
@@ -28,9 +35,14 @@
2835
.sw*
2936
.vscode
3037
/checkouts
38+
/checkouts/
3139
/classes
40+
/classes/
41+
/lib/
42+
/out
3243
/src/gen
3344
/target
45+
/target/
3446
Brewfile.lock.json
3547
cljs-test-runner-out/
3648
node_modules/

.markdownlint-cli2.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
config:
3+
MD013: false
4+
ignores:
5+
- node_modules/**

.pre-commit-config.yaml

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,52 @@
11
repos:
2-
- repo: https://github.com/clj-kondo/clj-kondo
3-
rev: v2023.10.20
4-
hooks:
5-
- id: clj-kondo-docker
6-
pass_filenames: false
7-
require_serial: true
82
- repo: https://github.com/pre-commit/pre-commit-hooks
9-
rev: v4.5.0
3+
rev: v6.0.0
104
hooks:
115
- id: check-shebang-scripts-are-executable
126
- id: trailing-whitespace
137
- id: end-of-file-fixer
148
- id: check-added-large-files
159
- repo: https://github.com/scop/pre-commit-shfmt
16-
rev: v3.7.0-4
10+
rev: v3.12.0-2
1711
hooks:
18-
- id: shfmt-docker
19-
entry: mvdan/shfmt:v3.7.0
12+
- id: shfmt
2013
args: [-w, -s, -i, "2"]
2114
- repo: https://github.com/koalaman/shellcheck-precommit
22-
rev: v0.9.0
15+
rev: v0.11.0
2316
hooks:
2417
- id: shellcheck
2518
- repo: local
2619
hooks:
20+
- id: clj-kondo
21+
name: clj-kondo
22+
entry: bin/_clj-kondo --lint src test bin/update-blocklist build.clj deps.edn tests.edn shadow-cljs.edn
23+
language: system
24+
pass_filenames: false
25+
require_serial: true
2726
- id: cljstyle
2827
name: cljstyle
2928
entry: bin/_cljstyle fix
3029
language: system
3130
types: [file]
31+
- id: kaocha-test
32+
name: kaocha-test
33+
entry: bin/kaocha
34+
language: system
35+
pass_filenames: false
36+
always_run: true
37+
require_serial: true
38+
- id: markdownlint-cli2
39+
name: markdownlint-cli2
40+
language: node
41+
entry: markdownlint-cli2
42+
additional_dependencies: ["markdownlint-cli2@0.21.0"]
43+
types: [markdown]
44+
args: ["--config", ".markdownlint-cli2.yaml", "--fix"]
3245
- id: prettier
3346
name: prettier
34-
language: docker_image
35-
entry: tmknom/prettier
47+
language: node
48+
entry: prettier
49+
additional_dependencies: ["prettier@3.8.1"]
3650
types: [text]
3751
args: [--write, --list-different, --ignore-unknown]
3852
- id: git-diff

AGENTS.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
5+
- Core library code lives in `src/org/sqids/`.
6+
- Cross-platform API is in `clojure.cljc`; specs are colocated in owning `.cljc` namespaces (`alphabet`, `block_list`, `encoding`, `decoding`, `init`, `results`).
7+
- Platform-specific helper functions are in `platform.clj` (JVM) and `platform.cljs` (CLJS).
8+
- Sqids algorithm logic is implemented in-repo (`alphabet.cljc`, `encoding.cljc`, `decoding.cljc`, `block_list.cljc`, `init.cljc`).
9+
- Bundled data assets are in `resources/` (for example `resources/org/sqids/clojure/blocklist.json`).
10+
- Tests are under `test/org/sqids/clojure/*_test.cljc`.
11+
- Tooling scripts are in `bin/` (`setup`, `kaocha`, `repl`, `_clj-kondo`, `_cljstyle`, `update-blocklist`).
12+
13+
## Build, Test, and Development Commands
14+
15+
- `bin/setup`: installs local prerequisites (`brew bundle`, `npm install`) on macOS/Homebrew setups.
16+
- `bin/kaocha`: runs all Kaocha suites (JVM `clojure.test`, automatic `clojure.spec.test.check`, and CLJS) with coverage output in `target/coverage/` and JUnit XML in `target/test-results/junit.xml`.
17+
- `bin/repl clj` / `bin/repl cljs`: starts interactive REPLs.
18+
- `bin/update-blocklist`: refreshes `resources/org/sqids/clojure/blocklist.json` from `sqids-spec`.
19+
- `pre-commit run --all-files`: runs local hooks; CI mirrors this as `SKIP=kaocha-test pre-commit run --all-files` followed by `bin/kaocha`.
20+
- `clojure -T:build jar` and `clojure -T:build install`: build and install the jar locally.
21+
22+
## Project Patterns
23+
24+
- Treat `AGENTS.md` as a repo table of contents, not an encyclopedia: point to the system-of-record files for behavior, build, tests, lint, and generated data before adding new prose here.
25+
- Prefer checked-in repo knowledge over chat or PR context; if code, tests, and docs disagree, the repository wins and stale guidance should be fixed in the same change.
26+
- Treat `bin/kaocha` as the single test entrypoint for local runs, pre-commit, and CI; avoid re-introducing split CLJ/CLJS wrappers.
27+
- Keep `tests.edn` as the source of truth for suite and report behavior (`:unit`, `:cljs`, `:generative-fdef-checks`, cloverage, JUnit XML target).
28+
- Keep test automation in sync: when pre-commit hook IDs or test commands change, update `.github/workflows/clojure.yml` (`SKIP=...`) in the same commit.
29+
- Prefer local/system hooks and wrappers over Docker hooks (`clj-kondo`, `cljstyle`, `shfmt`, `prettier`); avoid adding Docker-based hook runners.
30+
- Move invariants into mechanical enforcement whenever possible; prefer `clj-kondo`, `cljstyle`, pre-commit, Kaocha, and CI checks over reviewer memory or PR prose.
31+
- Keep algorithm code total in internal namespaces: only public `org.sqids.clojure/{sqids,encode,decode}` should throw.
32+
- Model internal success/failure with `org.sqids.clojure.results` envelopes and staged `results/conform`, `results/bind`, `results/attempt` pipelines.
33+
- Keep tool versions pinned in project config (`deps.edn`, `.pre-commit-config.yaml`); CI may use `latest` for bootstrap tools, but lint/format/test versions should remain explicit.
34+
- Treat `resources/org/sqids/clojure/blocklist.json` as generated data; update it via `bin/update-blocklist` rather than manual edits.
35+
- Add nested `AGENTS.md` files only when a subtree has materially different rules or maintenance needs; keep them short, additive, and scoped to local deltas rather than copying the root guide.
36+
- Pair every `AGENTS.md` with a human-facing `README.md` in the same scope. `AGENTS.md` directs agent behavior; `README.md` explains layout, intent, and workflows for humans.
37+
- When adding a new subsystem, ship code, tests, lint/config enforcement, docstrings, and navigation docs together.
38+
- Keep docs aligned with tooling changes: update `README.md` and this file in the same PR when commands or test flow change.
39+
- When behavior changes, update tests, docstrings, and README examples in the same commit so the contract stays synchronized.
40+
- Preserve public API behavior for `sqids`, `encode`, and `decode`; behavior changes must include deterministic tests and release notes updates.
41+
42+
## Coding Style & Naming Conventions
43+
44+
- Follow `.cljstyle`: 2-space indentation and one blank padding line between top-level forms.
45+
- Run formatting before commits: `bin/_cljstyle fix`.
46+
- Lint with pre-commit (includes `clj-kondo`, `cljstyle`, `kaocha-test`, `shellcheck`, `shfmt`, `markdownlint-cli2`, `prettier`, and a clean `git-diff` check).
47+
- `clj-kondo` is intentionally strict in `.clj-kondo/config.edn`; run `bin/_clj-kondo --lint src test bin/update-blocklist build.clj deps.edn tests.edn shadow-cljs.edn` before pushing.
48+
- Every `def`, `defn`, and `defmacro` needs a high-quality docstring. Treat docstrings as living contracts: explain purpose, inputs, return shape, invariants, and failure semantics when they are not obvious from the name alone.
49+
- Keep docstring enforcement strict. If `clj-kondo` stops catching missing docstrings on new function-like forms, tighten the linter or hooks instead of weakening the rule.
50+
- Namespace/file naming follows Clojure conventions: `kebab-case` namespaces and `*_test.cljc` test files.
51+
52+
## Testing Guidelines
53+
54+
- Primary framework: `clojure.test` executed by Kaocha; generative spec checks run via a `:kaocha.type/spec.test.check` suite.
55+
- Coverage is enforced at 100% via `:cloverage/opts :fail-threshold`; CI artifacts include `target/coverage/lcov.info` and `target/coverage/codecov.json`.
56+
- Add tests beside related behavior in `test/org/sqids/clojure/`.
57+
- Keep tests deterministic and cover both encode/decode behavior and invalid-input paths.
58+
- Before opening a PR, run `bin/kaocha`.
59+
60+
## Commit & Pull Request Guidelines
61+
62+
- Match existing history: short, imperative commit subjects (for example `Fix sqids-javascript link`, `Improve caching`).
63+
- Keep commits focused; separate refactors from behavior changes when possible.
64+
- PRs should include: purpose, key changes, and verification steps/commands run.
65+
- Link relevant issues when applicable and update `CHANGELOG.md` for release-facing changes.

Brewfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# frozen_string_literal: true
22

33
tap 'borkdude/brew'
4+
tap 'babashka/brew'
45

56
brew 'borkdude/brew/clj-kondo'
7+
brew 'babashka/brew/babashka'
68
brew 'clojure'
79
brew 'pre-commit'
810

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# CHANGELOG
22

3-
**v1.0.15**:
3+
## Unreleased
4+
5+
## v1.1.0
6+
7+
- Replaced external runtime wrappers with a shared pure Clojure/ClojureScript
8+
Sqids implementation.
9+
- Added a local default blocklist resource and expanded spec-alignment tests.
10+
- Updated contributor/development documentation and script usage text.
11+
12+
## v1.0.15
413

514
- Initial implementation

0 commit comments

Comments
 (0)