How to wrap one schema inside another for Malli? - clojure

This is my payload.
(m/validate readingDetails-schema
{:readingCode : "Code1",
:readingNumber : "twenty two round off",
:readingCheck : "First",
:readings : [{
:readingDate : "2021-05-02",
:readingType : "first check",
:readingPrecision : "two decimals check",
:readingEstimate : "Check"}]})
This is my first schema:
(def reading-schema (m/schema [:map
[:readingDate :re #"\d{4}-\d{2}-\d{2}"]
[:readingType string?]
[:readingPrecision decimal?]
[:readingEstimate string?]]))
=> #'interest-malli-test.core/reading-schema
Now this is the main schema, which must include the above schema.
(def readingDetails-schema [:merge
#ref [:reading-schema]
[:map
[:readingCode string?]
[:readingNumber string?]
[:readingCheck string?]
[<how to add a list of reading-schema here?>]
]])
I'm trying like this.
(def readingDetails-schema [:merge
#ref [:reading-schema]
[:map
[:readingCode string?]
[:readingNumber string?]
[:readingCheck string?]
[[:vector #ref [:reading-schema]]
]])

After you convert the payload into Clojure data structures, just use the name of the first schema in the second one (and change decimal? into string?).
Dependencies: [metosin/malli "0.8.9"]
Require: [malli.core :as m]
(def reading-schema
[:map
[:readingDate :re #"\d{4}-\d{2}-\d{2}"]
[:readingType string?]
[:readingPrecision string?]
[:readingEstimate string?]])
(def readingDetails-schema
[:map
[:readingCode string?]
[:readingNumber string?]
[:readingCheck string?]
[:readings [:vector reading-schema]]])
(m/validate readingDetails-schema
{:readingCode "Code1",
:readingNumber "twenty two round off",
:readingCheck "First",
:readings [{:readingDate "2021-05-02",
:readingType "first check",
:readingPrecision "two decimals check",
:readingEstimate "Check"}]})
=> true

Related

How to filter a collection of maps into group-by map by value?

Lets say I have a collection like:
(def xs
[{:name "Apple" :type "Fruit is a type"}
{:name "Tomato" :type "Vegetable are food"}
{:name "Pear" :type "the type can also be Fruit"}
{:name "Steak" :type "eat less Meat"}])
And I want to filter and group-by the collection into something like this:
{:Fruit [{:name "Apple" :type "Fruit is a type"} {:name "Pear" :type "the type can also be Fruit"}] :Vegetable [{:name "Tomato" :type "Vegetable are food"}]
I currently just filter the results but can't seem to figure out a good way to group-by. Here's what I have so far:
(defn filter-response [x query]
(filter #(s/includes? (:type %) query) x))
(defn group-by-types [queries]
(map #(filter-response xs %) queries))
(group-by-types ["Fruit" "Vegetable"])
How can I accomplish this?
Updated Answer
You can use a list comprehension to check each item in the collection for each pattern.
(defn- all-occurrences [xs patterns]
(for [x xs
pattern patterns
:when (clojure.string/includes? (:type x) pattern)]
[(keyword pattern) x]))
Or using your filter-response function:
(defn- all-occurrences [xs patterns]
(for [pattern patterns
x (filter-response xs pattern)]
[(keyword pattern) x]))
Then use reduce with update to merge the list of occurrences into a single map:
(defn group-by-patterns [xs patterns]
(reduce (fn [m [pattern text]] (update m pattern conj text))
{}
(all-occurrences xs patterns)))
Calling it with the new input:
(def xs
[{:name "Apple" :type "Fruit is a type"}
{:name "Tomato" :type "Vegetable are food"}
{:name "Pear" :type "the type can also be Fruit"}
{:name "Steak" :type "eat less Meat"}])
(group-by-patterns xs ["Fruit" "Vegetable"])
=> {:Fruit ({:name "Pear", :type "the type can also be Fruit"} {:name "Apple", :type "Fruit is a type"}),
:Vegetable ({:name "Tomato", :type "Vegetable are food"})}
Original Answer
First you can use group-by to group by values under specified keys:
(def xs
[{:name "Apple" :type "Fruit"}
{:name "Tomato" :type "Vegetable"}
{:name "Pear" :type "Fruit"}
{:name "Steak" :type "Meat"}])
erdos=> (group-by :type xs)
{"Fruit" [{:name "Apple", :type "Fruit"} {:name "Pear", :type "Fruit"}],
"Vegetable" [{:name "Tomato", :type "Vegetable"}],
"Meat" [{:name "Steak", :type "Meat"}]}
Then use select-keys to filter the keys:
erdos=> (select-keys (group-by :type xs) ["Fruit" "Vegetable"])
{"Fruit" [{:name "Apple", :type "Fruit"} {:name "Pear", :type "Fruit"}],
"Vegetable" [{:name "Tomato", :type "Vegetable"}]}
If you need keyword keys, you need an extra mapping step:
erdos=> (into {}
(for [[k v] (select-keys (group-by :type xs) ["Fruit" "Vegetable"])]
[(keyword k) v]))
{:Fruit [{:name "Apple", :type "Fruit"} {:name "Pear", :type "Fruit"}],
:Vegetable [{:name "Tomato", :type "Vegetable"}]}

How do I generate user friendly validation messages with plumatic/schema?

I would like to be able to generate user-friendly or specify custom error messages for validation errors in these schemas:
(def Uuid (s/constrained String #(re-matches #"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" (name %))))
(def FirstName s/Str)
(def LastName s/Str)
(s/defschema Person {(s/required-key :id) Uuid,
(s/required-key :first-name) FirstName,
(s/required-key :last-name) LastName})
Valid schema:
{
:uuid "e143499c-1257-41e4-b951-c9e586994ff9"
:first-name "john"
:last-name "smith"
}
Invalid schema:
{
:uuid ""
:first-name nil
:last-name nil
}
Invalid schema - Errors:
{
"id" : "(not (app.person/fn--4881 \"\"))",
"first-name" : "(not (instance? java.lang.String nil))"
"last-name" : "(not (instance? java.lang.String nil))"
}
I would like to be able to generate something a bit more readable to non-programmers, for example:
{
"id" : "invalid uuid",
"first-name" : "must be a string"
"last-name" : "must be a string"
}
Funnily exactly this was released as a library a few days ago.
See:
https://github.com/siilisolutions/humanize
First you also need to tag your Uuid schema so you can match it later on:
;; Note the last param I added:
(def Uuid (sc/constrained
String
#(re-matches #"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
(name %))
'UUID))
(require '[schema.core :as sc]
'[humanize.schema :as hs])
(#'hs/explain (sc/check Person {:id "foo"
:first-name "foo"
:last-name 3})
(fn [x]
(clojure.core.match/match
x
['not ['UUID xx]]
(str xx " is not a valid UUID")
:else x)))
Results in:
=> {:id "foo is not a valid UUID", :last-name "'3' is not a string but it should be."}
Note, it needed a little trick since hs/explain is private unfortunately.

Why is my or-spec only valid for one of the given specs?

Consider the following spec for a text or a link layer port number:
(require '[clojure.spec.alpha :as spec])
(spec/def ::text (spec/and string? not-empty))
(spec/valid? ::text "a") ; => true
(spec/valid? ::text "") ; => false
(spec/def ::port (spec/and pos-int? (partial > 65535)))
(spec/valid? ::port 4) ; => true
(spec/valid? ::port 0) ; => false
(spec/def ::text-or-port (spec/or ::text ::port))
(spec/valid? ::text-or-port 5) ; => true
(spec/valid? ::text-or-port "hi") ; => false
For some reason it only accepts port-numbers and not text, why would that be?
The key to understanding this problem can be found in in the documentation and using spec/conform.
(spec/conform ::text-or-port 5)
; => [:user/text 5]
The problem is that clojure.spec.alpha/or has an API which is dissimmilar to clojure.core/or which given two arguments returns the first truthy one:
(#(or (string? %) (integer? %)) 5) ; => true
(#(or (string? %) (integer? %)) "") ; => true
(#(or (string? %) (integer? %)) :a) ; => false
Rather it takes pairs of labels and specs/predicates. And since even namespaced keywords are accepted as labels the ::text-or-port spec given in the OP matched only that which passed the requirements for ::port and gave it the label ::text. Below is a correct spec for that which we want to match:
(spec/def ::text-or-port (spec/or :text ::text
:port ::port))
(spec/valid? ::text-or-port "hi") ; => true
(spec/valid? ::text-or-port 10) ; => true

How to properly (unit) test Om/React components?

I have developed Om/React components, but I feel really uncomfortable not being able to drive my development with unit tests. I have tried to setup my clojurescript project to run unit tests on those components, and so far reached the point where I am able to write unit tests and instantiate my components. What I am missing is the ability to ensure my components properly react to some events, e.g. onChange so that I can simulate user inputs.
Here is my test code:
(defn simulate-click-event
"From https://github.com/levand/domina/blob/master/test/cljs/domina/test.cljs"
[el]
(let [document (.-document js/window)]
(cond
(.-click el) (.click el)
(.-createEvent document) (let [e (.createEvent document "MouseEvents")]
(.initMouseEvent e "click" true true
js/window 0 0 0 0 0
false false false false 0 nil)
(.dispatchEvent el e))
:default (throw "Unable to simulate click event"))))
(defn simulate-change-event
"From https://github.com/levand/domina/blob/master/test/cljs/domina/test.cljs"
[el]
(let [document (.-document js/window)]
(cond
(.-onChange el) (do (print "firing on change on " el) (.onChange el))
(.-createEvent document) (let [e (.createEvent document "HTMLEvents")]
(print "firing " e " on change on " (.-id el))
(.initEvent e "change" true true)
(.dispatchEvent el e))
:default (throw "Unable to simulate change event"))))
(def sink
"contains a channel that receives messages along with notification type"
(chan))
;; see http://yobriefca.se/blog/2014/06/04/publish-and-subscribe-with-core-dot-asyncs-pub-and-sub/
(def source
(pub sink #(:topic %)))
(defn change-field!
[id value]
(let [el (sel1 (keyword (str "#" id)))]
(dommy/set-value! el value)
(simulate-change-event el)
))
(deftest ^:async password-confirmation
(testing "do not submit if passwords are not equal"
(let [subscription (chan)]
(sub source :user-registration subscription)
(om/root
(partial u/registration-view source sink)
nil
{:target (sel1 :#view)})
(go
(let [m (<! subscription)]
(is (= :error (:state m)))
(done)
))
(change-field! "userRequestedEmail" "foo#bar.com")
(change-field! "userRequestedPassword" "secret")
(change-field! "confirmPassword" "nosecret")
(simulate-click-event (sel1 :#submitRegistration))
)))
This test runs but fails because the change-field! function does not actually change the state of the component. Here is (part of) the code of the component (forgive duplication...):
(defn registration-view
"Registration form for users.
Submitting form triggers a request to server"
[source sink _ owner]
(reify
om/IInitState
(init-state [_]
{:userRequestedEmail ""
:userRequestedPassword ""
:confirmPassword ""}
)
om/IRenderState
(render-state
[this state]
(dom/fieldset
nil
(dom/legend nil "User Registration")
(dom/div #js { :className "pure-control-group" }
(dom/label #js { :for "userRequestedEmail" } "EMail")
(dom/input #js { :id "userRequestedEmail" :type "text" :placeholder "Enter an e-mail"
:value (:userRequestedEmail state)
:onChange #(om/set-state! owner :userRequestedEmail (.. % -target -value))}))
(dom/div #js { :className "pure-control-group" }
(dom/label #js { :for "userRequestedPassword" } "Password")
(dom/input #js { :id "userRequestedPassword" :type "password" :placeholder "Enter password"
:value (:userRequestedPassword state)
:onChange #(om/set-state! owner :userRequestedPassword (.. % -target -value))}))
(dom/div #js { :className "pure-control-group" }
(dom/label #js { :for "confirmPassword" } "")
(dom/input #js { :id "confirmPassword" :type "password" :placeholder "Confirm password"
:value (:confirmPassword state)
:onChange #(om/set-state! owner :confirmPassword (.. % -target -value))}))
(dom/button #js {:type "submit"
:id "submitRegistration"
:className "pure-button pure-button-primary"
:onClick #(submit-registration state sink)}
"Register")))))
What I can see by putting traces in the tests is that the state of the component is not updated when I trigger the change event, although it is correctly triggered. I suspect this has to do with the way Om/React works, wrapping DOM components, but not sure how to deal with this.
You can mock events in your components using ReactTestUtils from the react libraries.
I'm using mocha and doing something like this to test change events:
var comp = ReactTestUtils.renderIntoDocument(<Component />);
var changingElement = ReactTestUtils.findRenderedDOMComponentWithClass(comp, 'el-class');
it ('calls myChangeMethod on change', function() {
ReactTestUtils.Simulate.change(changingElement);
assert(comp.myChangeEventMethod.called, true);
}

Get all properties in an object array

I have a clojure object array; something similar to:
(def data {
:genre "fantasy"
:books [
{ :id 1 :name "Lord of the rings" }
{ :id 2 :name "Game of thrones" }
{ :id 3 :name "Harry potter" }]
})
I want to get all the id of books.. something like [1 2 3]
I've tried a few things:
(seq (data :books :id))
(data :books) :id) ;results in an error, as expected
But I can't figure out how to get the ids. Do I have to use a doseq or other iterator function to get the ids?
Thanks
(->> data :books (map :id))