Skip to content

About

A trimmed port of metosin/malli to Squint (ClojureScript syntax, plain JS data)

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

squint-malli

A trimmed port of metosin/malli 0.20.1 to Squint: ClojureScript syntax compiled to plain JavaScript with plain JS data (strings for keywords, arrays, objects, Set).

It keeps malli's schema syntax, validation, explain and humanize, parse and unparse, transformers, schema utilities and JSON Schema output. It drops the parts that need a Clojure runtime or that make no sense once keywords are strings.

Status: experimental. The test suite passes (npm test). Not published to npm.

What is included

Namespace Contents
malli.core schemas, validate, explain, parse/unparse, walk, decode/encode, registries
malli.error humanize, error-value, spell-checking suggestions
malli.transform string and JSON transformers, strip-extra-keys, default values, key transformers
malli.json-schema schema to JSON Schema
malli.util merge, union, select-keys, closed-schema, get-in, assoc-in, update-properties, ...
malli.registry simple, composite, mutable, dynamic and lazy registries

Schema types: :any :some :nil :string :int :float :double :boolean, :> :>= :< :<= := :not=, :and :or :orn :not :maybe :enum :re :fn, :map :map-of :vector :sequential :set :tuple :multi :ref :schema ::m/schema.

What was dropped

  • Sequence (regex) schemas :cat :alt :* :+ :? :repeat :catn :altn and malli.impl.regex.
  • Function schemas :=> :-> :function, instrumentation, malli.generator, malli.dev, malli.provider, malli.destructure, malli.experimental.*, malli.instrument.
  • The AST protocol (m/ast, m/from-ast).
  • SCI evaluation. :fn children, :multi dispatch functions, :error/fn, :default/fn and :decode/* / :encode/* hooks must be real functions. :multi :dispatch may also be a key, which is looked up with get.
  • Predicate schemas (int?, string?, ...), bare regex literals as schemas, :andn, :seqable, :every, and the :merge / :union / :select-keys schema types (the functions in malli.util remain).
  • Types that cannot be distinguished from :string in Squint: :keyword :symbol :qualified-keyword :qualified-symbol :uuid.

Semantics that differ from malli on the JVM or ClojureScript

  • Keywords are strings. [:map [:id :int]] is ["map", ["id", "int"]] from JS, (m/type s) returns "map", and error types are strings such as "malli.core/missing-key".
  • All map keys are strings. A :map-of with non-string keys sees stringified keys.
  • Sets compare elements by reference, so :enum only works for primitives.
  • :double and :float accept any JS number. :int requires Number.isInteger.
  • The invalid sentinel returned by parse is the string "malli.core/invalid".
  • Schemas carry no metadata and do not print specially.

Usage

From Squint:

(ns app
  (:require [malli.core :as m]
            [malli.error :as me]
            [malli.transform :as mt]))

(def User [:map [:id :int] [:tags [:vector :string]]])

(m/validate User {:id 1 :tags ["a"]})                        ; => true
(-> (m/explain User {:id "x"}) me/humanize)                  ; => {:id ["should be an integer"]}
(m/decode User {:id "1" :tags []} (mt/string-transformer))   ; => {:id 1 :tags []}

From JavaScript, after npm run compile:

import * as m from './dist/malli/core.mjs';
import * as me from './dist/malli/error.mjs';
import * as mt from './dist/malli/transform.mjs';

const User = ["map", ["id", "int"], ["tags", ["vector", "string"]]];
m.validate(User, {id: 1, tags: ["a"]});                      // true
me.humanize(m.explain(User, {id: "x"}));                     // {id: ["should be an integer"]}
m.decode(User, {id: "1", tags: []}, mt.string_transformer()); // {id: 1, tags: []}

Squint munges names the ClojureScript way: string-transformer becomes string_transformer, map-of stays the string "map-of".

Build and test

npm install
npm run compile   # compiles src/ and test/ into dist/
npm test          # compile, then run dist/malli/run_tests.mjs
npm run size      # esbuild bundle sizes for three entry points

Bundle sizes (esbuild, minified, ES module), of which about 59 KB minified and 18 KB gzipped is Squint's own core library:

Entry Minified Gzipped
validate only 83.7 KB 22.9 KB
validate + humanize 98.1 KB 26.0 KB
validate + humanize + transform + JSON Schema 119.0 KB 31.6 KB

Keeping up with malli releases

src/malli is generated from a malli jar by a mechanical script plus one patch of hand edits, so a new malli release can be synced with npm run sync -- <version>. The report says what changed upstream in the kept code and which hand edits no longer apply. See SYNCING.md.

Porting notes

The sources come from the malli 0.20.1. Reader conditionals were resolved to their :cljs branch, the namespaces listed above were removed with a rewrite-clj script, and the remaining changes fall into a handful of mechanical categories:

  • Maps, sets and keywords used as functions ((@cache k), (m x), (ks k), :message as a default function) became get and contains? calls.
  • implements? became satisfies?; MapEntry, keyword-identical?, goog-define, IPrintWithWriter, unchecked-* arithmetic and hash-combine were replaced or removed.
  • Fully qualified protocol names such as malli.core.Schema lost their prefix.
  • The :clj macro that registers predicate schemas became a runtime reduce, then was dropped with the predicate schemas.
  • Index-keyed maps in :tuple became vectors, because every object key is a string.
  • A bare type name such as :int is the string "int" and used to look like a registry reference. m/schema now only treats a string as a reference when the registry entry is not a schema constructor.

Squint behaviors discovered while porting, which the code now works around:

  • Destructuring with a local symbol as the map key, {{:keys [..]} method}, looks up the literal string "method".
  • A local named fn is compiled as a call to a core function.
  • A def named eval is illegal in strict-mode ES modules.
  • reduce-kv ignores reduced and yields string indices for vectors. malli.impl.util/-reduce-kv is used instead throughout malli.core.
  • map? and coll? return true for reify instances, so protocol checks must come first.
  • Calling a map, set or keyword as a function only works when the compiler can see a literal at the call site.

Feedback

This is an experiment and feedback is welcome, especially on which dropped features you would miss, and on the Squint workarounds listed above. Please open an issue at https://github.com/kpassapk/squint-malli/issues.

License

Eclipse Public License 2.0, the same license as malli. The original work is copyright Metosin Oy. See LICENSE.

About

A trimmed port of metosin/malli to Squint (ClojureScript syntax, plain JS data)

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages