Use of prismatic schema => - clojure

What is the intended use of the => macro?
https://github.com/plumatic/schema/blob/master/src/cljx/schema/core.cljx#L1077-L1091
It seems to pass as long as a function is passed, for instance:
(s/defschema MySchema s/Int)
(def a (s/=> MySchema s/Num))
(s/validate a :ok)

Related

Using Plumatic Schema to coerce to bigdec

I have incoming data of the type {:loan/amount 1200}.
Would it be possible to use plumatic Schema to coerce this to {:loan/amount 1200M}, ie coercing numbers (or even strings of digits) to bigdecimals?
I don't how to define a new data type (like s/Bigdec) and then make sure it uses clojure.core/bigdec to coerce a certain value to a java.math.BigDecimal.
There are two separate concepts in Schema: validation and coercion.
For the first one you need to define your schema. Classes are treated as schemas so you don't need to create a custom one for java.math.BigDecimal. Your schema might look like:
(require '[schema.core :as s])
(require '[schema.coerce :as c])
(s/defschema Loan {:loan/amount java.math.BigDecimal})
Now you can validate your data against the schema:
(s/validate Loan {:loan/amount 10M})
;; => {:loan/amount 10M}
Now if you have some data that you would like to coerce you need to define a coercion function which is a matcher from a desired target schema (java.math.BigDecimal in your case) to a function that will convert the actual value to the desired bigdec value.
(def safe-bigdec (c/safe bigdec)
schema.coerce/safe is a utility function which wraps the original function and if the original one throws an exception when called, safe will return original input value instead of throwing the exception.
Our matcher function will check if the current schema element is BigDecimal and return converting function or nil otherwise (meaning there is no coercion for other types):
(defn big-decimal-matcher [schema]
(when (= java.math.BigDecimal schema)
safe-bigdec))
And finally we need a coercer for performing actual coercion:
(def loan-coercer (c/coercer Loan big-decimal-matcher))
With all the setup we can now use our coercer:
(loan-coercer {:loan/amount "12.34"})
;; => {:loan/amount 12.34M}
(loan-coercer {:loan/amount 1234})
;; => {:loan/amount 1234M}
(loan-coercer {:loan/amount "abc"})
;; => #schema.utils.ErrorContainer{:error {:loan/amount (not (instance? java.math.BigDecimal "abc"))}}

validation of clojure schema

i have a problem in validation of clojure prismatic schema. Here is the code.
:Some_Var1 {:Some_Var2 s/Str
:Some_Var3 ( s/conditional
#(= "mytype1" (:type %)) s/Str
#(= "mytype2" (:type %)) s/Str
)}
I am trying to validate it using the code:
"Some_Var1": {
"Some_Var2": "string",
"Some_Var3": {"mytype1":{"type":"string"}}
}
but it is throwing me an error:
{
"errors": {
"Some_Var1": {
"Some_Var3": "(not (some-matching-condition? a-clojure.lang.PersistentArrayMap))"
}
}
}
It is a very basic code i am trying to validate. I am very new to clojure and still trying to learn basics of it.
Thanks,
Welcome to Clojure! It's a great language.
In Clojure, a keyword and a string are distinct types i.e. :type is not the same "type". For example:
user=> (:type {"type" "string"})
nil
(:type {:type "string"})
"string"
However, I think there is a deeper issue here: from looking at your data, it appears you want to encode the type information in the data itself and then check it based on that information. It might be possible, but it would be a pretty advanced usage of schema. Schema is typically used when the types are known ahead of type e.g. data like:
(require '[schema.core :as s])
(def data
{:first-name "Bob"
:address {:state "WA"
:city "Seattle"}})
(def my-schema
{:first-name s/Str
:address {:state s/Str
:city s/Str}})
(s/validate my-schema data)
I'd suggest that if you need validate based on encoded type information, it'd probably be easier to write a custom function for that.
Hope that helps!
Update:
An an example of how conditional works, here's a schema that will validate, but again, this is a non-idiomatic use of Schema:
(s/validate
{:some-var3
(s/conditional
;; % is the value: {"mytype1" {"type" "string"}}
;; so if we want to check the "type", we need to first
;; access the "mytype1" key, then the "type" key
#(= "string" (get-in % ["mytype1" "type"]))
;; if the above returns true, then the following schema will be used.
;; Here, I've just verified that
;; {"mytype1" {"type" "string"}}
;; is a map with key strings to any value, which isn't super useful
{s/Str s/Any}
)}
{:some-var3 {"mytype1" {"type" "string"}}})
I hope that helps.

Accessing value from defn

I need advice,
I try to make function :
(def user-map [new-name new-phone new-email]
{:name new-name
:phone new-phone
:email new-email})
With new-name, new-phone, new-email are user input. But when i try to compile it, it says too many arguments to def, after change def to defn, when i try to execute user-map in REPL i get something like
#<temp_atom$user_address zenedu.mm.dbase.temp_atom$user_address#714924b5
instead of actual map.
I need to get to the map, any advice?
It sounds like perhaps you are conceptually combining the value that will be returned from calling user-map as a function with some arguments and evaluating the symbol user-map on its own.
Evaluating
(user-map "me" "123456789" "me#here.com")
Which will return a map, by looking up the var user-map in the current namespace and calling the function stored in that var with these arguments. Where evaluating just
user-map
Will simply look up the var user-map in the current namespace and return the contents of that var, which in the case where you used defn, will be the function it's self. The REPL then prints the object I'd of that function.
In your use case, you need defn to define a builder (like a constructor in Java) for the object you want. The log
#<temp_atom$user_address zenedu.mm.dbase.temp_atom$user_address#714924b5
suggests that you are using another structure user-address somewhere in the application and it looks like there is confusion between a user-map object and this user-address.
Anyway, you may be interested to have a look at defrecord that provides a convenient way to build objects with a constructor (and potentially other functions related to this object), e.g.
(defrecord user [name phone email])
defrecord provides 2 constructors ->user and map->user:
(def me (->user "abb" "0102030405" "abb#mail.com"))
(def you (map->user {:email "das#mail.com" :phone "9090909090" :name "das"}))
And you can access the properties of a user through keywords exactly like a map:
user> (:name me)
"abb"
user> (:phone you)
"9090909090"
OK, you should use defn instead of def.
But what information really varies here? The number and order of the map keys, in this case [:name :phone :email].
A generic function that will build - from a key sequence - a function that will build the map from the value sequence is
(defn map-builder [keys]
(fn [& values] (zipmap keys values)))
You can then define
(def user-map (map-builder [:name :phone :email]))
... which works as required:
(user-map "me" "123456789" "me#here.com")
;{:email "me#here.com", :phone "123456789", :name "me"}
If performance is pressing, by all means use records instead of maps, as #AbbéRésina suggests.
Putting it simply...
The error you are receiving is due to essentially passing a vector as a second value to def. If you want to use def in this instance go with...
(def user-map-other
(fn [new-name new-phone new-email]
{:name new-name
:phone new-phone
:email new-email}))
Here we are using an anonymous function that accepts your three parameters. Here is a link to learn more about them => http://clojuredocs.org/clojure.core/fn
To gain access to the values contained in your function we can use get in this instance.
(get (user-map-other "Ben" "999" "bbb#mail.com") :phone) => "999"
(get (user-map-other "Ben" "999" "bbb#mail.com") :name) => "Ben"
(get (user-map-other "Ben" "999" "bbb#mail.com") :email) => "bbb#mail.com"
A more concise method would be to use defn as represented below.
(defn user-map [new-name new-phone new-email]
{:name new-name
:phone new-phone
:email new-email})
(get (user-map "Ben" "999" "bbb#mail.com") :phone) => "999"
(get (user-map "Ben" "999" "bbb#mail.com") :name) => "Ben"
(get (user-map "Ben" "999" "bbb#mail.com") :email) => "bbb#mail.com"

Function validation using Prismatic/schema in Clojure

I have a very simple question about using Prismatic/schema to validate functions. I have a schema for a map that has a single key, the value of which is a function that takes a Bar schema as its single argument and returns anything (used for side effects):
(require '[schema.core :as s])
(def Bar {:baz s/Int})
(def Action :???)
(def Foo {:action Action})
The question is, how do I define Action? I've tried this:
(require '[schema.macros :as sm])
(def Action (sm/=> s/Any Bar))
This looks promising, but I can't get it to fail validation:
(s/explain Action)
;=> (=> Any {:baz Int})
;; This should fail
(s/validate Foo {:action :anything-goes})
;=> {:action :anything-goes}
What am I doing wrong here?
I read the docs and the tests in core_test, but I can't figure out how to do this.
I've found this: https://github.com/Prismatic/schema/blob/a21cc0113ed497f6410c55d92d9088bd710f0b47/src/cljx/schema/core.cljx#L888
So it would be something like:
(def Action (s/make-fn-schema s/Any [[Bar]]))
Although, the documentation does say this:
Currently function schemas are purely descriptive; they validate against any function, regardless of actual input and output types

Use of loose schemas in Clojure with clj-schema

I want to create what I believe is called a loose schema to verify the contents of a Clojure map.
The clj-schema examples led me to believe that I only needed to specify :loose as the first argument to def-map-schema. However this does not seem to work.
Consider the following test (using midje):
(def-map-schema loose-schema :loose [[:id] Number])
(fact (empty? (validation-errors loose-schema {:id 1})) => true) ; OK
(fact (empty? (validation-errors loose-schema {:id "string"})) => false) ; OK
(fact (empty? (validation-errors loose-schema {:id 1 :foo "bar"})) => true) ; NO!
The third test produces:
"Path [:foo] was not specified in the schema."
How can I make the schema ignore extraneous map entries?
I had the arguments around the wrong way.
The schema returned by def-map-schema was not actually loose:
(loose-schema? loose-schema)
=> false
Instead of:
(def-map-schema loose-schema :loose [[:id] Number])
The correct form is:
(def-map-schema :loose loose-schema [[:id] Number])