Skip to content

Commit

Permalink
Merge pull request #1071 from frenchy64/issue-1070-map-enum-form
Browse files Browse the repository at this point in the history
Correctly form prop-less schemas that have map/nil as first child
  • Loading branch information
ikitommi authored Jul 19, 2024
2 parents 0dd2e0d + 34c6e09 commit 0eef516
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 6 deletions.
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Data-driven Schemas for Clojure/Script and [babashka](#babashka).
- [Inferring Schemas](#inferring-schemas) from sample values and [Destructuring](#destructuring).
- Tools for [Programming with Schemas](#programming-with-schemas)
- [Parsing](#parsing-values) and [Unparsing](#unparsing-values) values
- [Sequence](#sequence-schemas), [Vector](#vector-schemas), and [Set](#set-schemas) Schemas
- [Enumeration](#enumeration-schemas), [Sequence](#sequence-schemas), [Vector](#vector-schemas), and [Set](#set-schemas) Schemas
- [Persisting schemas](#persisting-schemas), even [function schemas](#serializable-functions)
- Immutable, Mutable, Dynamic, Lazy and Local [Schema Registries](#schema-registry)
- [Schema Transformations](#schema-Transformation) to [JSON Schema](#json-schema), [Swagger2](#swagger2), and [descriptions in english](#description)
Expand Down Expand Up @@ -329,6 +329,32 @@ Most core-predicates are mapped to Schemas:

See [the full list of default schemas](#schema-registry).

## Enumeration schemas

`:enum` schemas `[:enum V1 V2 ...]` represent an enumerated set of values `V1 V2 ...`.

This mostly works as you'd expect, with values passing the schema if it is contained in the set and generators returning one of the values,
shrinking to the left-most value.

There are some special cases to keep in mind around syntax. Since schema properties can be specified with a map or nil, enumerations starting with
a map or nil must use slightly different syntax.

If your `:enum` does not have properties, you must provide `nil` as the properties.

```clojure
[:enum nil {}] ;; singleton schema of {}
[:enum nil nil] ;; singleton schema of nil
```

If your `:enum` has properties, the leading map with be interpreted as properties, not an enumerated value.

```clojure
[:enum {:foo :bar} {}] ;; singleton schema of {}, with properties {:foo :bar}
[:enum {:foo :bar} nil] ;; singleton schema of nil, with properties {:foo :bar}
```

In fact, these syntax rules apply to all schemas, but `:enum` is the most common schema where this is relevant so it deserves a special mention.

## Qualified keys in a map

You can also use [decomplected maps keys and values](https://clojure.org/about/spec#_decomplect_mapskeysvalues) using registry references. References must be either qualified keywords or strings.
Expand Down
7 changes: 6 additions & 1 deletion src/malli/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,12 @@
(let [has-children (seq children), has-properties (seq properties)]
(cond (and has-properties has-children) (reduce conj [type properties] children)
has-properties [type properties]
has-children (reduce conj [type] children)
has-children (let [fchild (nth children 0)]
(reduce conj
(cond-> [type]
(or (map? fchild)
(nil? fchild)) (conj nil))
children))
:else type)))

(defn -create-form [type properties children options]
Expand Down
34 changes: 30 additions & 4 deletions test/malli/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -545,10 +545,36 @@
#_(is (= 1 (m/decode schema "1" mt/string-transformer)))
#_(is (= "1" (m/decode schema "1" mt/json-transformer)))

(testing "map enums require nil properties"
(let [schema [:enum nil {:a 1} {:b 2}]]
(is (= nil (m/properties schema)))
(is (= [{:a 1} {:b 2}] (m/children schema)))))
(testing "nil enums without properties require empty properties"
(let [schema [:enum nil nil]]
(testing (pr-str schema)
(is (= nil (m/properties schema)))
(is (= [nil] (m/children schema)))
(is (= schema (m/form schema)))
(is (= schema (-> schema m/form m/schema m/form))))))

(testing "nil nums support properties"
(let [schema [:enum {:foo :bar} nil]]
(is (= {:foo :bar} (m/properties schema)))
(is (= [nil] (m/children schema)))
(is (= schema (m/form schema)))
(is (= schema (-> schema m/form m/schema m/form)))))

(testing "map enums without properties require empty properties"
(doseq [schema [[:enum nil {:a 1} {:b 2}]
[:enum {} {:a 1} {:b 2}]]]
(testing (pr-str schema)
(is (= nil (m/properties schema)))
(is (= [{:a 1} {:b 2}] (m/children schema)))
(is (= [:enum nil {:a 1} {:b 2}] (m/form schema)))
(is (= [:enum nil {:a 1} {:b 2}] (-> schema m/form m/schema m/form))))))

(testing "map enums support properties"
(let [schema [:enum {:foo :bar} {:a 1} {:b 2}]]
(is (= {:foo :bar} (m/properties schema)))
(is (= [{:a 1} {:b 2}] (m/children schema)))
(is (= schema (m/form schema)))
(is (= schema (-> schema m/form m/schema m/form)))))

(is (true? (m/validate (over-the-wire schema) 1)))

Expand Down

0 comments on commit 0eef516

Please sign in to comment.