Skip to content
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7969487
Account for vector (sub)schemas in `with-paths` fn
marksto Oct 4, 2025
c45fbc1
Avoid duplicate paths in the `key-seqs` fn result
marksto Oct 4, 2025
edab5db
Untangle the `->idiomatic` key fn impl + use helper
marksto Oct 4, 2025
09dfdeb
Enable parameter aliases for `st/default` schemas
marksto Oct 4, 2025
eacac72
Add more tests for the parameter aliases feature
marksto Oct 4, 2025
04a7686
Make `walk-with-path` fn resemble `schema-tools.walk/walk`
marksto Oct 4, 2025
883dbb0
Minor improvements of `with-paths` fn impl
marksto Oct 4, 2025
afd66f4
Update all docstrings and related ToDo items
marksto Oct 4, 2025
4c5b260
Fix compilation error for ClojureScript
marksto Oct 4, 2025
6d8d215
Cover the `martian.schema-tool/key-seqs` fn with tests
marksto Oct 5, 2025
2ecf1fb
Cover the `martian.schema-tool/prewalk-with-path` fn with tests
marksto Oct 5, 2025
332d3b1
Fix a typo in the `prewalk-with-path-test` name
marksto Oct 6, 2025
0579763
Test for all sorts of key types in map schemas
marksto Oct 6, 2025
ddf76bc
Add/update test cases for `st/default` schemas
marksto Oct 6, 2025
726064c
Add test cases for `named` schemas
marksto Oct 6, 2025
34a97b3
Add test cases for `maybe` schemas
marksto Oct 6, 2025
b823695
Add test cases for `constrained` schemas
marksto Oct 6, 2025
ef5917e
Add test cases for `both`/`either` schemas
marksto Oct 7, 2025
7c0e4a2
Add test cases for `cond-pre` schemas
marksto Oct 7, 2025
b24d513
Add test cases for `conditional` schemas
marksto Oct 8, 2025
ffe9032
Improve on the existing test coverage
marksto Oct 8, 2025
4e1bbb5
Rename the `unspecify-key` fn to `explicit-key`
marksto Oct 8, 2025
99b6a57
Re-impl the `key-seqs` function with a new protocol
marksto Oct 8, 2025
ba4bd43
Cover non-keyword and generic (schema) keys
marksto Oct 8, 2025
1beecd3
Improve performance — `key-seqs` fn & `-paths` method
marksto Oct 11, 2025
f843ae5
Improve performance — `parameter-aliases` fn
marksto Oct 11, 2025
4842d26
Re-impl `parameter-aliases` with lazy registry
marksto Oct 13, 2025
95bdbd9
Reshape the existing `:parameter-aliases` test case
marksto Oct 13, 2025
367c7da
Add some way to return param aliases as map (fixes BB)
marksto Oct 14, 2025
a5ad9f4
Add support for `schema.core.Recursive` schemas
marksto Oct 14, 2025
cbcb972
Fix Babashka tests + introduce a recursion limit
marksto Oct 14, 2025
8ca01bc
Improve and fine tune the `aliases-hash-map` fn
marksto Oct 14, 2025
ec32339
Improve on recursive schemas test's completeness
marksto Oct 15, 2025
9c4fda4
Re-impl the `aliases-hash-map` fn via prev protocol
marksto Oct 15, 2025
71d6438
Misc improvements (names, docstrings, arglists)
marksto Oct 15, 2025
afb6984
Re-enable `cond-pre` schemas tests for Babashka
marksto Oct 15, 2025
b9d6df0
Add more test coverage for HTTP headers mapping
marksto Oct 21, 2025
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
4 changes: 2 additions & 2 deletions core/src/martian/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[lambdaisland.uri :refer [map->query-string]]
[martian.interceptors :as interceptors]
[martian.openapi :refer [openapi->handlers openapi-schema?]]
[martian.parameter-aliases :refer [parameter-aliases alias-schema]]
[martian.parameter-aliases :refer [registry alias-schema]]
[martian.schema :as schema]
[martian.spec :as mspec]
[martian.swagger :refer [swagger->handlers]]
Expand Down Expand Up @@ -133,7 +133,7 @@

(defn- collect-parameter-aliases [handler]
(reduce (fn [aliases param-key]
(assoc aliases param-key (parameter-aliases (get handler param-key))))
(assoc aliases param-key (registry (get handler param-key))))
{}
parameter-schemas))

Expand Down
153 changes: 113 additions & 40 deletions core/src/martian/parameter_aliases.cljc
Original file line number Diff line number Diff line change
@@ -1,54 +1,127 @@
(ns martian.parameter-aliases
(:require [schema.core :as s]
[camel-snake-kebab.core :refer [->kebab-case]]
[clojure.set :refer [rename-keys]]
[martian.schema-tools :refer [key-seqs prewalk-with-path]]))
(:require [clojure.set :refer [rename-keys]]
[martian.schema-tools :as schema-tools]
[schema.core :as s]))

;; todo lean on schema-tools.core for some of this
Copy link
Contributor Author

@marksto marksto Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not possible since schema-tools don't provide any utilities for traversing schemas with path, while we heavily rely on this kind of traversal for both schemas and data. Other schema-related utilities were moved out of this namespace into the martian.schema-tools, leveraging schema-tools to some degree.

(defn- aliases-at
"Internal helper. Given a `schema`, a path-local `cache` (atom), an `interner`
(atom), and a `path` (vector or seq), returns the alias map for that path.
(defn ->idiomatic [k]
(when (and k (s/specific-key? k) (not (and (keyword? k) (namespace k))))
(->kebab-case (s/explicit-schema-key k))))
- Delegates the actual computation to `compute-aliases-at`, which understands
schema wrappers (e.g. `:schema`, `:schemas`, etc.) and vector transparency.
- Caches results per-path in the provided `cache` to avoid recomputation.
- Interns identical alias maps across the paths via `interner`, so equal maps
share a single canonical instance (reduces memory churn in large APIs).
- Returns `nil` when there are no aliases at the given path."
[schema cache interner path]
(let [path' (if (vector? path) path (vec path))]
(or (get @cache path')
(let [m (schema-tools/compute-aliases-at schema path')
m' (when m
(or (get @interner m)
(-> interner
(swap! #(if (contains? % m) % (assoc % m m)))
(get m))))]
(swap! cache assoc path' m')
m'))))

#?(:bb nil

:clj
(deftype LazyRegistry [schema cache interner]
clojure.lang.ILookup
(valAt [_ k]
(aliases-at schema cache interner k))
(valAt [_ k not-found]
(or (aliases-at schema cache interner k) not-found))
Object
(toString [_] (str "#LazyRegistry (cached " (count @cache) ")")))

:cljs
(deftype LazyRegistry [schema cache interner]
cljs.core/ILookup
(-lookup [_ k]
(aliases-at schema cache interner k))
(-lookup [_ k not-found]
(or (aliases-at schema cache interner k) not-found))
cljs.core/IPrintWithWriter
(-pr-writer [_ writer _opts]
(-write writer (str "#LazyRegistry (cached " (count @cache) ")")))))

(defn- idiomatic-path [path]
(vec (keep ->idiomatic path)))
(vec (keep schema-tools/->idiomatic path)))

(defn parameter-aliases
"Produces a data structure for use with `unalias-data`"
(defn aliases-hash-map
"Eagerly computes the registry as a data structure for the given `schema`.
Produces a plain hash map with idiomatic keys (aliases) mappings per path
in a (possibly, deeply nested) `schema` for all its unqualified keys.
The result is then used with `alias-schema` and `unalias-data` functions."
[schema]
(reduce (fn [acc path]
(if-let [idiomatic-key (some-> path last ->idiomatic)]
(if-not (= (last path) idiomatic-key)
(update acc (idiomatic-path (drop-last path)) merge {idiomatic-key (last path)})
acc)
acc))
(let [leaf (peek path)
idiomatic-key (some-> leaf (schema-tools/->idiomatic))]
(if (and idiomatic-key (not= leaf idiomatic-key))
(update acc (idiomatic-path (pop path)) assoc idiomatic-key leaf)
acc)))
{}
(key-seqs schema)))
(schema-tools/key-seqs schema)))

Comment on lines 19 to +70
Copy link
Contributor Author

@marksto marksto Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation does the same thing, but faster (uses faster peek/pop on path vectors and faster assoc instead of merge).

(defn registry
"Builds a lookupable registry of parameter alias maps for the given `schema`.
- On JVM/CLJS:
Returns an instance of a lazy registry.
Aliases are computed on demand (via `compute-aliases-at`), so materializing
massive alias maps upfront is avoided. Per-path results are memoized within
the registry. Identical alias maps are shared to cut memory usage.
A returned value implements `ILookup` and is indexed by \"idiomatic paths\".
Looking up a path gives an alias map for that level, mapping idiomatic keys
(kebab-case, unqualified) to their original schema keys.
- On Babashka:
Returns a plain hash map registry that is computed eagerly via `key-seqs`."
[schema]
(when schema
#?(:bb (aliases-hash-map schema)
:default (new LazyRegistry schema (atom {}) (atom {})))))
Comment on lines +89 to +90
Copy link
Contributor Author

@marksto marksto Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Babashka does not yet support interfaces implementations on custom records/types (defrecord/deftype currently only support protocol implementations, found: clojure.lang.ILookup), so we have to opt out of laziness for it and use a plain hash map in order for parameter aliases (registry) to preserve the map(-like) semantics/structure.


;; TODO: An alias for backward compatibility. Remove later on.
(def parameter-aliases registry)

(defn unalias-data
"Takes parameter aliases and (deeply nested) data, returning data with deeply-nested keys renamed as described by parameter-aliases"
[parameter-aliases x]
(if parameter-aliases
(prewalk-with-path (fn [path x]
(if (map? x)
(rename-keys x (get parameter-aliases (idiomatic-path path)))
x))
[]
x)
x))
"Given a (possibly, deeply nested) `data` structure, returns it with all its
keys renamed from \"idiomatic\" (aliases) using the given parameter aliases
`registry`."
[registry data]
(if registry
(schema-tools/prewalk-with-path
(fn [path x]
(if (map? x)
(rename-keys x (get registry (idiomatic-path path)))
x))
data)
data))

(defn alias-schema
"Walks a schema, transforming all keys into their aliases (idiomatic keys)"
[parameter-aliases schema]
(if parameter-aliases
(prewalk-with-path (fn [path x]
(if (map? x)
(let [kmap (reduce-kv (fn [kmap k v]
(assoc kmap v k (s/optional-key v) (s/optional-key k)))
{}
(get parameter-aliases (idiomatic-path path)))]
(rename-keys x kmap))
x))
[]
schema)
"Given a (possibly, deeply nested) `schema`, renames all keys (in it and its
subschemas) into corresponding \"idiomatic\" keys (aliases) using the given
parameter aliases `registry`."
[registry schema]
(if registry
(schema-tools/prewalk-with-path
(fn [path subschema]
(if (map? subschema)
(let [kmap (reduce-kv (fn [kmap idiomatic original]
(assoc kmap
original idiomatic
(s/optional-key original) (s/optional-key idiomatic)))
{}
(get registry (idiomatic-path path)))]
(rename-keys subschema kmap))
subschema))
schema)
schema))
Loading