Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: ohpauleez/themis
base: b06b0387dd
...
head fork: ohpauleez/themis
compare: 8310394eb0
Checking mergeability… Don't worry, you can still create the pull request.
  • 2 commits
  • 4 files changed
  • 0 commit comments
  • 1 contributor
View
107 guide.mkd
@@ -0,0 +1,107 @@
+# Themis
+
+Themis is built to process data structures, be it for validation, response
+generation, a system dispatching.
+
+Data validation is the most common use. This document will walk you through
+using Themis to validate a data structure and generate a new data structure
+based on the results of validation.
+
+The example will specifically validate a hash-map, but any data structure that
+is participates with Themis' navigation protocol will work.
+
+
+## Beginner Example
+
+Themis validates a data structure, usually a map, with a given validation
+ruleset. A basic example:
+
+```clj
+(defn required-field
+ [payload data-point options]
+ (when (empty? data-point)
+ {:error "Field is required"}))
+
+(def person {:name "Bob" :age 20})
+(def rules [[:name [required-field]]]])
+
+(themis.core/validation (dissoc person :name) rules)
+; => {[:name] {:error "Field is required"}}
+
+(themis.core/validation person rules)
+; => nil
+```
+
+As you can see, rules are a vectors of subvectors. Each subvector is in the format
+`[:coordinate [validation-fn1 validation-fn2 ...]]`.
+
+Note `:coordinate` is `:name` in this example. However a `:coordinate` can also be a vector. For
+example, if you wanted to put a rule on the first name of `{:name {:first "Bob"}}`, the coordinates
+would be `[:name :first]`.
+
+`required-field` is a validation fn. All themis validation fns _must_ take three arguments:
+
+* The first argument, `payload`, is the whole data structure being validated. In this case it would be
+ the person map.
+* The second argument, `data-point`, is the value of the payload at that key/coordinate. In the examples above,
+ those values would be nil and "Bob".
+* The third argument, `options`, is an options map. By default, themis provides the current
+ key/coordinate being validated on e.g. `{:themis.core/coordinates [:name]}`. See
+ [Extras](#extras) for more.
+
+## Intermediate Example
+
+Now that you know about themis rules and validation fns, let's look at an example with more than one
+validation fn:
+
+```clj
+;; Additional validation fn
+(defn ensure-price-for-certain-upcs
+ [payload data-point options]
+ (let [{:keys [price upc_code] payload]
+ (when (and (upc_code (.startsWith upc_code "A") (< price 100)))
+ {:error "Upc A* must at least be $100"}))
+
+(def item {:upc_code "A23C" :price 200.00})
+(def rules [[:upc_code [required-field]]
+ [:* [ensure-price-for-certain-upcs]])
+
+(themis.core/validation (assoc item :price 40.00) rules)
+; => {[:*] {:error "Upc A* must at least be $100"}}
+
+(themis.core/validation item rules)
+; => nil
+```
+
+In this example, `ensure-price-for-certain-upcs` is a validation fn that uses the first argument, the
+whole payload, instead of just one value. For this validation, `:*` is only being used to report
+the error back on that key. Themis is _agnostic_ to the coordinates for validations. You may use
+whatever general key you want, such as `:all` or `:multi`, but it must not match an existing key/coordinate
+in the data structure.
+
+## Validators
+
+Themis comes with validations and validation builders in `themis.validators`. For example,
+`validators/required` checks for presence on the given coordinate, and
+`validators/hard-presence` for a coordinate with a non-nil/non-empty value.
+
+Validation builders are functions which given arguments will generate a validation. One such handy
+builder is `validators/from-predicate`. Given a function and an error response, it will apply the
+function to the current coordinate value. One may use this to build up domain specific validators.
+
+## Extras
+
+These are a list of useful things to know about themis in no particular order:
+
+* You can pass options (3rd argument) to validation fns in the rules.
+ The format for a single validation with options is
+ `[validation-fn options-map]`. Thus in a rule it would look like: `[[:coordinate [[validation-fn
+ options-map] validation-fn2]]]`.
+* To run rules in parallel, themis provides `themis.core/pvalidation`.
+* You may use the function `reckon` and `preckon` in place of `validation` and `pvalidation`
+ if that makes more sense in your application (ie: if you're not directly doing validation)
+* When declaring multiple validation fns for a coordinate, themis will run _all_ of them. Therefore,
+ when writing validation fns don't assume anything about your data e.g. it's a vector or a string
+ unless it has already been validated in a previous function.
+* See [themis tests](https://github.com/ohpauleez/themis/blob/master/test/themis/core_test.clj) for examples of additional abilities themis has.
+
View
30 src/themis/core.clj
@@ -132,6 +132,20 @@
See also: `pvalidation`"}
preckon pvalidation)
+(defn unfold-result
+ "Unfold the themis results map, expanding coordinates to nested maps,
+ and remove `nil` results"
+ [themis-result-map]
+ (reduce (fn [old [k-vec value]]
+ (let [validation-value (remove nil? value)
+ seqd-value (not-empty validation-value)]
+ (if seqd-value
+ (assoc-in old k-vec
+ (if (sequential? value)
+ (vec seqd-value)
+ value))
+ old)))
+ nil themis-result-map))
(comment
@@ -167,22 +181,6 @@
(= (validation paul paul-rules) (pvalidation paul paul-rules))
(mapcat identity (validation paul paul-rules :merge-fn (partial filter second)))
(validation paul paul-rules :merge-fn (partial keep second))
-
-
- (defn unfold-result
- "Unfold the themis results map, expanding coordinates to nested maps,
- and remove `nil` results"
- [themis-result-map]
- (reduce (fn [old [k-vec value]]
- (let [validation-value (remove nil? value)
- seqd-value (not-empty validation-value)]
- (if seqd-value
- (assoc-in old k-vec
- (if (sequential? value)
- (vec seqd-value)
- value))
- old)))
- nil themis-result-map))
(unfold-result (validation paul paul-rules))
)
View
38 src/themis/predicates.clj
@@ -3,32 +3,58 @@
;; Predicates
;; -------------
;;
-;; It is often easier to reason about validtion composing smaller
+;; It is often easier to reason about validation composing smaller
;; predicate functions. Below you'll find common ones supplied by Themis.
;;
;; These also serve as an example of how two write application specific
;; validators
-(defn longer-than? [t length]
+(defn longer-than?
+ "Returns true if an item, `t`, is longer than some `length`, non-inclusive"
+ [t length]
(> (count t) length))
-(defn shorter-than? [t length]
+(defn shorter-than?
+ "Returns true if an item, `t`, is shorter than some `length`, non-inclusive"
+ [t length]
(< (count t) length))
-(defn length? [t length]
+(defn length?
+ "Returns true if an item, `t`, is as long as `length`"
+ [t length]
(= (count t) length))
(defn length-between?
+ "Returns true if an item, `t`, is within some length, inclusive.
+ If you only supply a high-bound, the low defaults to 0"
([t high]
(length-between? t 0 high))
([t low high]
(let [length (count t)]
(<= low length high))))
-(defn is-in? [t & items]
+(defn is-in?
+ "Returns true if an item, `t`, is matched/present in a collection of `items`"
+ [t & items]
(and (some #{t} items)
true))
-(defn is-not-in? [t & items]
+(defn is-not-in?
+ "Returns true if an item, `t`, is not matched/present in a collection of `items`"
+ [t & items]
(not (some #{t} items)))
+(def non-neg-integer?
+ "Returns true if an integer and >= 0"
+ (every-pred integer? (complement neg?)))
+
+(defn boolean? [t]
+ (instance? java.lang.Boolean t))
+
+(defn format?
+ "Returns true if a string, `s`, is of a specific format; if it matches a regex string.
+ If the regex string is passed in as a string, it will be converted to a RegEx object"
+ [s regex-str]
+ (let [regex-str (if (string? regex-str) (re-pattern regex-str) regex-str)]
+ (boolean (re-find regex-str s))))
+
View
16 src/themis/validators.clj
@@ -59,6 +59,13 @@
(when-not (apply f data-point arg-data (butlast more-data))
(response (last more-data) {})))))
+(defn weak-predicate
+ "Given a predicate function that takes a single arg,
+ return a proper validator for it, that is also accepts
+ nil (non-present) values"
+ ([f & args]
+ (apply from-predicate (some-fn nil? f) args)))
+
;; Validation functions
;; ---------------------
;;
@@ -104,7 +111,12 @@
(empty? data-point))
(response "required value is empty" opt-map)))
-(comment
+(defn hard-presence
+ "Required, Present, and Non-empty"
+ [t data-point opt-map]
+ (when (or (required t data-point opt-map)
+ (presence t data-point opt-map)
+ (non-empty t data-point opt-map))
+ (response "required coordinate/value is not present or is empty" opt-map)))
- )

No commit comments for this range

Something went wrong with that request. Please try again.