Home

Awesome

schema-refined

CircleCI

Powerful "refined" steroids for schema library.

Type refinement is all about making the types (schemas) more precise to keep you away from errors and bugs. All heavy lifting of checking/validation the shape of the data is done by schema library. Here we introduce a few new concepts to make schemas flexible and more expressive:

And more!

Add to your project with Leiningen/Boot:

[com.attendify/schema-refined "0.3.0-alpha4"]

or with deps.edn

com.attendify/schema-refined {:mvn/version "0.3.0-alpha4"}

Our Goals

Talks and Tutorials

Inspired By

Usage

Get ready!

(require '[schema-refined.core :as r])
(require '[schema.core :as s])

Refined

schema-refined.core/refined is a supercharged version of schema.core/constrained. This function takes two params: a type (which should be a valid schema) and a predicate (which should either satisfy schema-refiend.core/Predicate protocol or be a function from value of given type to boolean) and returns a schema that checks both that "basic" schema (given as a type) is satisfied and the predicates returns true for this specific value. You can also use another schema as a predicate. There are a lot of built-in predicates, please check the listing below. Predicates are composable, you can create a new one from existing using logical rules And, Or, Not and On (checks predicate after applying to the value given function). There're also a few high-level predicaetes to deal with collections, like Forall, First, Last etc.

Motivational example.

;; "manually" with refined and predicates
(def LatCoord (r/refined double (r/OpenClosedInterval -90.0 90.0)))

;; the same using built-in types
;; (or functions to create types from other types, a.k.a. generics)
(def LngCoord (r/OpenClosedIntervalOf double -180.0 180.0))

;; Product type using a simple map
(def GeoPoint {:lat LatCoord :lng LngCoord})

;; using built-in types
(def Route (r/BoundedListOf GeoPoint 2 50))

;; or same with predicates
(def Route (r/refined [GeoPoint] (BoundedSize 2 50)))

(def input [{:lat 48.8529 :lng 2.3499}
            {:lat 51.5085 :lng -0.0762}
            {:lat 40.0086 :lng 28.9802}])

;; Route now is a valid schema, so you can use it as any other schema
(s/check Route input)

Even more motivational example.

(def InZurich {:lat (r/refined double (r/OpenInterval 47.34 47.39))
               :lng (r/refined double (r/OpenInterval 8.51 8.57))})

(def InRome {:lat (r/refined double (r/OpenInterval 41.87 41.93))
             :lng (r/refined double (r/OpenInterval 12.46 12.51))})

;; you can use schemas as predicates
(def RouteFromZurich (r/refined Route (r/First InZurich)))
(def RouteToRome (r/refined Route (r/Last InRome)))
(def RouteFromZurichToRome (r/refined Route (r/And (r/First InZurich) (r/Last InRome))))

;; or even more
;; note, that predicates are composable
(def FromZurichToRome (r/And (r/First InZurich) (r/Last InRome)))
(def RouteFromZurichToRomeWithLess3Hops
  (r/refined Route (r/And FromZurichToRome (r/BoundedSize 2 5))))

Naming Convention

The library follows a few rules on how names are made, so it's easier to make sense of types and predicates:

Sum Types

Schema previously had s/either to deal with sum types. Which didn't work the way e.g. one-of doesn't work when dealing with JSON schema: the description is fragile and error messages is not useful at all ("typing" message that given data does not conform any of the listed options would only confuse). That's why schema switch to conditional where you have to specify branching predicate in advance. schema-refined includes slightly more readable version of conditionals r/dispatch-on that covers the fundamental use case of having a single predicate to decide on the branch (option).

(def EmptyScrollableList
  {:items (s/eq [])
   :totalCount (s/eq 0)
   :hasNext (s/eq false)
   :hasPrev (s/eq false)
   :nextPageCursor (s/eq nil)
   :prevPageCursor (s/eq nil)})

(defn NonEmptyScrollableListOf [dt]
  (r/dispatch-on (juxt :hasNext :hasPrev)
    [false false] (SinglePageOf dt)
    [true  false] (FirstPageOf dt)
    [false true]  (LastPageOf dt)
    [true  true]  (ScrollableListSliceOf dt)))

(defn ScrollableListOf [dt]
  (r/dispatch-on :totalCount
    0 EmptyScrollableList
    :else (NonEmptyScrollableListOf dt)))

Product Types

schema-refined.core/Struct creates a product type which works like a simple map, but can be flexible refined with schema-refined.core/guard. Guarded struct still can be changed "on fly" using assoc (think: adding a new field to the record) and dissoc (think: removing specific field from the record).

(def -FreeTicket (r/Struct
                  :id r/NonEmptyStr
                  :type (s/eq "free")
                  :title r/NonEmptyStr
                  :quantity (r/OpenIntervalOf int 1 1e4)
                  :description (s/maybe r/NonEmptyStr)
                  :status (s/enum :open :closed)))

(def FreeTicket (r/guard -FreeTicket '(:quantity :status) enough-sits-when-open))

;; #<StructMap {:description (constrained Str should-not-be-blank)
;;              :type (eq "free")
;;              :title (constrained Str should-not-be-blank)
;;              :status (enum :open :closed)
;;              :id java.lang.String
;;              :quantity (constrained int should-be-bounded-by-range-given)}
;;   Guarded with
;;     enough-sits-when-open over '(:quantity :status)>

You can easily extend the type now:

(def -PaidTicket (assoc FreeTicket
                        :type (s/eq "paid")
                        :priceInCents r/PositiveInt
                        :taxes [Tax]
                        :fees (s/enum :absorb :pass)))

(def PaidTicket
  (r/guard -PaidTicket '(:taxes :fees) pass-tax-included))

;; #<StructMap {...}
;;   Guarded with
;;     enough-sits-when-open over '(:quantity :status)
;;     pass-tax-included over '(:taxes :fees)>

and reduce:

(dissoc PaidTicket :status)

;; #<StructMap {...}
;;   Guarded with
;;     pass-tax-included over '(:taxes :fees)>

;; (only one guard left)

schema-refined.core/StructDispatch provides you the same functionality as schema-refined.core/dispatch-on, but the resulting type behaves like a one created with schema-refined.core/Struct.

(def Ticket (r/StructDispatch :type
              "free" FreeTicket
              "paid" PaidTicket))

;; #<StructDispatch on '(:type):
;;     free => {...}
;;     paid => {...}>

;; note, that when using `schema.core/conditional` the following would not
;; give you intended result! but it works as expected here
(def CreateTicketRequest (dissoc Ticket :id :status))

More?

To find more examples and use cases, please see doc strings (whenever applicable) and tests.

Future Versions (a.k.a In Progress)

Appendix A: Builtin Predicates & Types

Predicate Combinators

Ordering Predicates

Numerical Predicates

Numerical Types

String Predicates

String Types

Collection Predicates

Collection Types

Contribute

or simply...

License

schema-refined is licensed under the MIT license, available at MIT and also in the LICENSE file.

Implementation of -def-map-type is based on Potemkin (Copyright © 2013 Zachary Tellman)