在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
开源软件名称:plumatic/schema开源软件地址:https://github.com/plumatic/schema开源编程语言:Clojure 99.5%开源软件介绍:A Clojure(Script) library for declarative data description and validation. -- One of the difficulties with bringing Clojure into a team is the overhead of understanding the kind of data (e.g., list of strings, nested map from long to string to double) that a function expects and returns. While a full-blown type system is one solution to this problem, we present a lighter weight solution: schemas. (For more details on why we built Schema, check out this post.) Schema is a rich language for describing data shapes, with a variety of features:
Meet SchemaA Schema is a Clojure(Script) data structure describing a data shape, which can be used to document and validate functions and data. (ns schema-examples
(:require [schema.core :as s
:include-macros true ;; cljs only
]))
(def Data
"A schema for a nested data type"
{:a {:b s/Str
:c s/Int}
:d [{:e s/Keyword
:f [s/Num]}]})
(s/validate
Data
{:a {:b "abc"
:c 123}
:d [{:e :bc
:f [12.2 13 100]}
{:e :bc
:f [-1]}]})
;; Success!
(s/validate
Data
{:a {:b 123
:c "ABC"}})
;; Exception -- Value does not match schema:
;; {:a {:b (not (instance? java.lang.String 123)),
;; :c (not (integer? "ABC"))},
;; :d missing-required-key} The simplest schemas describe leaf values like Keywords, Numbers, and instances of Classes (on the JVM) and prototypes (in ClojureScript): ;; s/Any, s/Bool, s/Num, s/Keyword, s/Symbol, s/Int, and s/Str are cross-platform schemas.
(s/validate s/Num 42)
;; 42
(s/validate s/Num "42")
;; RuntimeException: Value does not match schema: (not (instance java.lang.Number "42"))
(s/validate s/Keyword :whoa)
;; :whoa
(s/validate s/Keyword 123)
;; RuntimeException: Value does not match schema: (not (keyword? 123))
;; On the JVM, you can use classes for instance? checks
(s/validate java.lang.String "schema")
;; On JS, you can use prototype functions
(s/validate Element (js/document.getElementById "some-div-id")) From these simple building blocks, we can build up more complex schemas that look like the data they describe. Taking the examples above: ;; list of strings
(s/validate [s/Str] ["a" "b" "c"])
;; nested map from long to String to double
(s/validate {long {String double}} {1 {"2" 3.0 "4" 5.0}}) Since schemas are just data, you can also (def StringList [s/Str])
(def StringScores {String double})
(def StringScoreMap {long StringScores}) What about when things go bad? Schema's (s/validate StringList ["a" :b "c"])
;; RuntimeException: Value does not match schema:
;; [nil (not (instance? java.lang.String :b)) nil]
(s/validate StringScoreMap {1 {"2" 3.0 "3" [5.0]} 4.0 {}})
;; RuntimeException: Value does not match schema:
;; {1 {"3" (not (instance? java.lang.Double [5.0]))},
;; (not (instance? java.lang.Long 4.0)) invalid-key}
See the "More Examples" section below for more examples and explanation, or the custom Schemas types page for details on how Schema works under the hood. Beyond type hintsIf you've done much Clojure, you've probably seen code with documentation like this: (defrecord StampedNames
[^Long date
names ;; a list of Strings
])
(defn ^StampedNames stamped-names
"names is a list of Strings"
[names]
(StampedNames. (str (System/currentTimeMillis)) names)) Clojure's type hints make great documentation, but they fall short for complex types, often leading to ad-hoc descriptions of data in comments and doc-strings. This is better than nothing, but these ad hoc descriptions are often imprecise, hard to read, and prone to bit-rot. Schema provides macros (s/defrecord StampedNames
[date :- Long
names :- [s/Str]])
(s/defn stamped-names :- StampedNames
[names :- [s/Str]]
(StampedNames. (str (System/currentTimeMillis)) names)) Here, As you can see, these type hints are precise, easy to read, and shorter than the comments they replace. Moreover, they produce Schemas that are data, and can be inspected, manipulated, and used for validation on-demand (did you spot the bug in ;; You can inspect the schemas of the record and function
(s/explain StampedNames)
==> (record user.StampedNames {:date java.lang.Long, :names [java.lang.String]})
(s/explain (s/fn-schema stamped-names))
==> (=> (record user.StampedNames {:date java.lang.Long, :names [java.lang.String]})
[java.lang.String])
;; And you can turn on validation to catch bugs in your functions and schemas
(s/with-fn-validation
(stamped-names ["bob"]))
==> RuntimeException: Output of stamped-names does not match schema:
{:date (not (instance? java.lang.Long "1378267311501"))}
;; Oops, I guess we should remove that `str` from `stamped-names`. Schemas in practiceWe've already seen how we can build up Schemas via composition, attach them to functions, and use them to validate data. What does this look like in practice? First, we ensure that all data types that will be shared across namespaces (or heavily used within namespaces) have Schemas, either by This documentation is probably the most important benefit of Schema, which is why we've optimized Schemas for easy readability and reuse -- and sometimes, this is all you need. Schemas are purely descriptive, not prescriptive, so unlike a type system they should never get in your way, or constrain the types of functions you can write. After documentation, the next-most important benefit is validation. Thus far, we've found four key use cases for validation. First, you can globally turn on function validation within a given test namespace by adding this line: (use-fixtures :once schema.test/validate-schemas) As long as your tests cover all call boundaries, this means you should catch any 'type-like' bugs in your code at test time. Second, it may be handy to enable schema validation during development. To enable it, you can either type this into the repl or put it in your (s/set-fn-validation! true) To disable it again, call the same function, but with Third, we manually call Alternatively, you can force validation for key functions (without the need for (s/defn ^:always-validate stamped-names ...) Thus, each time you invoke To reduce generated code size, you can use the Schema will attempt to reduce the verbosity of its output by restricting the size of values that fail validation to 19 characters. If a value exceeds this, it will be replaced by the name of its class. You can adjust this size limitation by calling Finally, we use validation with coercion for API inputs and outputs. See the coercion section below for details. More examplesThe source code in schema/core.cljc provides a wealth of extra tools for defining schemas, which are described in docstrings. The file schema/core_test.cljc demonstrates a variety of sample schemas and many examples of passing & failing clojure data. We'll just touch on a few more examples here, and refer the reader to the code for more details and examples (for now). Map schema detailsIn addition to uniform maps (like String to double), map schemas can also capture maps with specific key requirements: (def FooBar {(s/required-key :foo) s/Str (s/required-key :bar) s/Keyword})
(s/validate FooBar {:foo "f" :bar :b})
;; {:foo "f" :bar :b}
(s/validate FooBar {:foo :f})
;; RuntimeException: Value does not match schema:
;; {:foo (not (instance? java.lang.String :f)),
;; :bar missing-required-key} For the special case of keywords, you can omit the (def FancyMap
"If foo is present, it must map to a Keyword. Any number of additional
String-String mappings are allowed as well."
{(s/optional-key :foo) s/Keyword
s/Str s/Str})
(s/validate FancyMap {"a" "b"})
(s/validate FancyMap {:foo :f "c" "d" "e" "f"}) Sequence schema detailsSimilarly, you can also write sequence schemas that expect particular values in specific positions: (def FancySeq
"A sequence that starts with a String, followed by an optional Keyword,
followed by any number of Numbers."
[(s/one s/Str "s")
(s/optional s/Keyword "k")
s/Num])
(s/validate FancySeq ["test"])
(s/validate FancySeq ["test" :k])
(s/validate FancySeq ["test" :k 1 2 3])
;; all ok
(s/validate FancySeq [1 :k 2 3 "4"])
;; RuntimeException: Value does not match schema:
;; [(named (not (instance? java.lang.String 1)) "s")
;; nil nil nil
;; (not (instance? java.lang.Number "4"))] Other schema types
;; anything
(s/validate [s/Any] ["woohoo!" 'go-nuts 42.0])
;; maybe
(s/validate (s/maybe s/Keyword) :a)
(s/validate (s/maybe s/Keyword) nil)
;; eq and enum
(s/validate (s/eq :a) :a)
(s/validate (s/enum :a :b :c) :a)
;; pred
(s/validate (s/pred odd?) 1)
;; conditional (i.e. variant or option)
(def StringListOrKeywordMap (s/conditional map? {s/Keyword s/Keyword} :else [String]))
(s/validate StringListOrKeywordMap ["A" "B" "C"])
;; => ["A" "B" "C"]
(s/validate StringListOrKeywordMap {:foo :bar})
;; => {:foo :bar}
(s/validate StringListOrKeywordMap [:foo])
;; RuntimeException: Value does not match schema: [(not (instance? java.lang.String :foo))]
;; if (shorthand for conditional)
(def StringListOrKeywordMap (s/if map? {s/Keyword s/Keyword} [String]))
;; cond-pre (experimental), also shorthand for conditional, allows you to skip the
;; predicate when the options are superficially different by doing a greedy match
;; on the preconditions of the options.
(def StringListOrKeywordMap (s/cond-pre {s/Keyword s/Keyword} [String]))
;; but don't do this -- this will never validate `{:b :x}` because the first schema
;; will be chosen based on the `map?` precondition (use `if` or `abstract-map-schema` instead):
(def BadSchema (s/cond-pre {:a s/Keyword} {:b s/Keyword}))
;; conditional can also be used to apply extra validation to a single type,
;; but constrained is often more desirable since it applies the validation
;; as a *postcondition*, which typically provides better error messages
;; and works better with coercion
(def OddLong (s/constrained long odd?))
(s/validate OddLong 1)
;; 1
(s/validate OddLong 2)
;; RuntimeException: Value does not match schema: (not (odd? 2))
(s/validate OddLong (int 3))
;; RuntimeException: Value does not match schema: (not (instance? java.lang.Long 3))
;; recursive
(def Tree {:value s/Int :children [(s/recursive #'Tree)]})
(s/validate Tree {:value 0, :children [{:value 1, :children []}]})
;; abstract-map (experimental) models "abstract classes" and "subclasses" with maps.
(require '[schema.experimental.abstract-map :as abstract-map])
(s/defschema Animal
(abstract-map/abstract-map-schema
:type
{:name s/Str}))
(abstract-map/extend-schema Cat Animal [:cat] {:claws? s/Bool})
(abstract-map/extend-schema Dog Animal [:dog] {:barks? s/Bool})
(s/validate Cat {:type :cat :name "melvin" :claws? true})
(s/validate Animal {:type :cat :name "melvin" :claws? true})
(s/validate Animal {:type :dog :name "roofer" :barks? true})
(s/validate Animal {:type :cat :name "confused kitty" :barks? true})
;; RuntimeException: Value does not match schema: {:claws? missing-required-key, :barks? disallowed-key} You can also define schemas for recursive data types, or create your own custom schemas types. Transformations and CoercionSchema also supports schema-driven data transformations, with coercion being the main application fleshed out thus far. Coercion is like validation, except a schema-dependent transformation can be applied to the input data before validation. An example application of coercion is converting parsed JSON (e.g., from an HTTP post request) to a domain object with a richer set of types (e.g., Keywords). (def CommentRequest
{(s/optional-key :parent-comment-id) long
:text String
:share-services [(s/enum :twitter :facebook :google)]})
(def parse-comment-request
(coerce/coercer CommentRequest coerce/json-coercion-matcher))
(= (parse-comment-request
{:parent-comment-id (int 2128123123)
:text "This is awesome!"
:share-services ["twitter" "facebook"]})
{:parent-comment-id 2128123123
:text "This is awesome!"
:share-services [:twitter :facebook]})
;; ==> true Here,
There's nothing special about For more details, see this blog post. For the FutureLonger-term, we have lots more in store for Schema. Just a couple of the crazy ideas we have brewing are:
CommunityPlease feel free to join the Plumbing mailing list to ask questions or discuss how you're using Schema. We welcome contributions in the form of bug reports and pull requests; please see
If you make something new, please feel free to PR to add it here! Supported Clojure versionsSchema is currently supported on Clojure 1.8 onwards and the latest version of ClojureScript. LicenseDistributed under the Eclipse Public License, the same as Clojure. |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论