Skip to content
Open
Show file tree
Hide file tree
Changes from 24 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
27 changes: 18 additions & 9 deletions core/src/martian/parameter_aliases.cljc
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
(ns martian.parameter-aliases
(:require [schema.core :as s]
[camel-snake-kebab.core :refer [->kebab-case]]
(:require [camel-snake-kebab.core :refer [->kebab-case]]
[clojure.set :refer [rename-keys]]
[martian.schema-tools :refer [key-seqs prewalk-with-path]]))
[martian.schema-tools :refer [explicit-key key-seqs prewalk-with-path]]
[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 can-be-renamed? [k]
;; NB: See `camel-snake-kebab.internals.alter-name` ns.
(or (and (keyword? k) (not (namespace k))) (string? k)))

(defn ->idiomatic [k]
(when (and k (s/specific-key? k) (not (and (keyword? k) (namespace k))))
(->kebab-case (s/explicit-schema-key k))))
(when-some [k' (when k (explicit-key k))]
(when (can-be-renamed? k')
(->kebab-case k'))))

(defn- idiomatic-path [path]
(vec (keep ->idiomatic path)))

(defn parameter-aliases
"Produces a data structure for use with `unalias-data`"
"Produces a data structure 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)]
Expand All @@ -26,7 +32,8 @@
(key-seqs schema)))

(defn unalias-data
"Takes parameter aliases and (deeply nested) data, returning data with deeply-nested keys renamed as described by parameter-aliases"
"Given a (possibly, deeply nested) data `x`, returns the data with all keys
renamed as described by the `parameter-aliases`."
[parameter-aliases x]
(if parameter-aliases
(prewalk-with-path (fn [path x]
Expand All @@ -38,7 +45,9 @@
x))

(defn alias-schema
"Walks a schema, transforming all keys into their aliases (idiomatic keys)"
"Given a (possibly, deeply nested) `schema`, renames all keys (in it and its
subschemas) into corresponding idiomatic keys (aliases) as described by the
`parameter-aliases`."
[parameter-aliases schema]
(if parameter-aliases
(prewalk-with-path (fn [path x]
Expand Down
191 changes: 156 additions & 35 deletions core/src/martian/schema_tools.cljc
Original file line number Diff line number Diff line change
@@ -1,50 +1,171 @@
(ns martian.schema-tools
(:require [schema.core :as s #?@(:cljs [:refer [MapEntry EqSchema]])]
[schema.spec.core :as spec])
#?(:clj (:import [schema.core MapEntry EqSchema])))

;; todo
;; write some tests and lean on schema-tools.core where possible
Comment on lines -6 to -7
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did just that.


(defn with-paths [path schema]
(keep (fn [schema]
(cond (and (instance? MapEntry schema)
(instance? EqSchema (:key-schema schema)))
{:path (conj path (:v (:key-schema schema)))
:schema (:val-schema schema)}
(map? schema)
{:path path
:schema schema}))
(spec/subschemas (s/spec schema))))
(:require [schema.core :as s]
[schema-tools.impl]))

(defn explicit-key [k]
(if (s/specific-key? k) (s/explicit-schema-key k) k))
Comment on lines +6 to +7
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The same helper as in the schema-tools.core.


(defn concrete-key? [k]
(or (keyword? k)
(s/specific-key? k)
(string? k)))

(defn- concat* [& xs]
(apply concat (remove nil? xs)))

(defprotocol KeyPaths
(-paths [schema path include-self?]
"Returns a sequence of path vectors found within the given prefix `path`.
If `include-self?` is true, includes `path` itself as the first element."))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Welcome the new protocol for proper keys retrieval (quite pitily absent in schema-tools).

Copy link
Contributor Author

@marksto marksto Oct 14, 2025

Choose a reason for hiding this comment

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

It's actually called PathAliases now and it goes with one new method. Please, consult the latest sources.


(extend-protocol KeyPaths
#?(:clj clojure.lang.APersistentMap
:cljs cljs.core.PersistentArrayMap)
(-paths [schema path include-self?]
(concat*
(when include-self? (list path))
(mapcat (fn [[k v]]
(when (concrete-key? k)
(let [k' (explicit-key k)
path' (conj path k')]
(cons path' (-paths v path' false)))))
schema)))

;; NB: Vector schemas are transparent (indices are ignored).
#?(:clj clojure.lang.APersistentVector
:cljs cljs.core.PersistentVector)
(-paths [schema path include-self?]
(concat*
(when include-self? (list path))
(mapcat #(-paths % path false) schema)))

schema.core.NamedSchema
(-paths [schema path include-self?]
(let [inner-schema (:schema schema)]
(concat*
(when include-self? (list path))
(-paths inner-schema (conj path :schema) true)
(-paths inner-schema path false))))

schema.core.Maybe
(-paths [schema path include-self?]
(let [inner-schema (:schema schema)]
(concat*
(when include-self? (list path))
(-paths inner-schema (conj path :schema) true)
(-paths inner-schema path false))))

schema.core.Constrained
(-paths [schema path include-self?]
(let [inner-schema (:schema schema)]
(concat*
(when include-self? (list path))
(-paths inner-schema (conj path :schema) true)
(-paths inner-schema path false))))

schema.core.One
(-paths [schema path include-self?]
(let [inner-schema (:schema schema)]
(concat*
(when include-self? (list path))
(-paths inner-schema (conj path :schema) true)
(-paths inner-schema path false))))

schema.core.Record
(-paths [schema path include-self?]
(let [inner-schema (:schema schema)]
(concat*
(when include-self? (list path))
(-paths inner-schema (conj path :schema) true)
(-paths inner-schema path false))))

schema.core.Both
(-paths [schema path include-self?]
(let [inner-schemas (:schemas schema)]
(concat*
(when include-self? (list path))
(mapcat #(-paths % (conj path :schemas) false) inner-schemas)
(mapcat #(-paths % path false) inner-schemas))))

schema.core.Either
(-paths [schema path include-self?]
(let [inner-schemas (:schemas schema)]
(concat*
(when include-self? (list path))
(mapcat #(-paths % (conj path :schemas) false) inner-schemas)
(mapcat #(-paths % path false) inner-schemas))))

schema.core.CondPre
(-paths [schema path include-self?]
(let [inner-schemas (:schemas schema)]
(concat*
(when include-self? (list path))
(mapcat #(-paths % (conj path :schemas) false) inner-schemas)
(mapcat #(-paths % path false) inner-schemas))))

schema.core.ConditionalSchema
(-paths [schema path include-self?]
(let [inner-schemas (map second (:preds-and-schemas schema))]
(concat*
(when include-self? (list path))
(mapcat #(-paths % (conj path :preds-and-schemas) false) inner-schemas)
(mapcat #(-paths % path false) inner-schemas))))

schema_tools.impl.Default
(-paths [schema path include-self?]
(let [inner-schema (:schema schema)]
(concat*
(when include-self? (list path))
(-paths inner-schema (conj path :schema) true)
(-paths inner-schema (conj path :value) true)
(-paths inner-schema path false))))

#?(:clj Object :cljs default)
(-paths [_ path include-self?]
(when include-self? (list path)))

nil
(-paths [_ _ _] nil))

(defn key-seqs
"Returns a collection of paths which would address all possible entries (using `get-in`) in data described by the schema"
"Returns a vec of unique key paths (key seqs) for `schema` and all subschemas
that will cover all possible entries in a data described by `schema` as well
as the `schema` itself."
[schema]
(when (map? schema)
(loop [paths [[]]
paths-and-schemas (with-paths [] schema)]
(if-let [{:keys [path schema]} (first paths-and-schemas)]
(recur (conj paths path) (concat (rest paths-and-schemas)
(with-paths path schema)))
paths))))
(->> (-paths schema [] true)
(distinct)
(vec)))

;;

(defn walk-with-path
"Identical to `clojure.walk/walk` except keeps track of the path through the data structure (as per `get-in`)
as it goes, calling `inner` and `outer` with two args: the path and form"
"Similar to the `schema-tools.walk/walk` except it keeps track of the `path`
through the data structure as it goes, calling `inner` and `outer` with two
args: the `path` and the `form`. It also does not preserve any metadata."
([inner outer form] (walk-with-path inner outer [] form))
([inner outer path form]
(cond
(list? form) (outer path (apply list (map (partial inner path) form)))
(map-entry? form)
(outer path #?(:clj (clojure.lang.MapEntry. (inner path (key form)) (inner (conj path (key form)) (val form)))
:cljs (cljs.core/MapEntry. (inner path (key form)) (inner (conj path (key form)) (val form)) nil)))
(seq? form) (outer path (doall (map (partial inner path) form)))
(record? form) (outer path (reduce (fn [r x] (conj r (inner path x))) form form))
(coll? form) (outer path (into (empty form) (map (partial inner path) form)))
(outer path [(inner path (key form))
(inner (conj path (key form)) (val form))])
(record? form)
(outer path (reduce (fn [r x] (conj r (inner path x))) form form))
(list? form)
(outer path (apply list (map #(inner path %) form)))
(seq? form)
(outer path (doall (map #(inner path %) form)))
(coll? form)
(outer path (into (empty form) (map #(inner path %) form)))
:else (outer path form))))
Comment on lines 334 to 352
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made the implementation of this function similar to schema-tools.walk/walk by reordering conditions and making the (map-entry? form) case simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I specifically checked that changing the order of clauses here does not affect the performance.


(defn postwalk-with-path [f path form]
(walk-with-path (partial postwalk-with-path f) f path form))
(walk-with-path (fn [path form] (postwalk-with-path f path form))
f
path
form))

(defn prewalk-with-path [f path form]
(walk-with-path (partial prewalk-with-path f) (fn [_path form] form) path (f path form)))
(walk-with-path (fn [path form] (prewalk-with-path f path form))
(fn [_path form] form)
path
(f path form)))
Loading