Problem with Clojure Spec about simple parameter matching - clojurescript

I'm struggling with Clojure(script) spec.
I slightly found out what part causes problem but I can't solve it.
(defn filter-ids
[[items fields] _]
(let [ids
(for [item items
field-tags (vals fields)
:let [item-tags (-> item second :tags)
item-id (first item)]
:when (and
(seq field-tags)
(empty? (set/difference field-tags item-tags)))]
item-id)]
(into #{} ids)))
Above code is what I tried to define spec. (fdef)
And I defined spec.
(spec/def :common/id (spec/and
keyword?
#(-> %1 name js/parseInt nat-int?)))
(spec/def :common/label string?)
(spec/def :common/tags (spec/coll-of string? :kind set?))
(spec/def :common/item (spec/keys :req-un [:common/label :common/tags]))
(spec/fdef filter-ids
:args (spec/cat
:useful (spec/cat
:items (spec/map-of :common/id :common/item)
:fields (spec/map-of :common/id :common/tags))
:useless any?)
:ret (spec/coll-of :common/id :kind set?))
And when I run it with instrument, error occurs.
(stest/instrument `filter-ids)
(filter-ids [{:0 {:label "task0" :tags #{"one" "two"}}}
{:0 #{"three"}, :1 #{"one"}}]
nil)
; Execution error - invalid arguments to taggy.states.subs/filter-ids at (<cljs repl>:1).
[{:0 {:label "task0", :tags #{"two" "one"}}} {:0 #{"three"}, :1 #{"one"}}] - failed: map? at: [:useful :items]
It seems like spec think first argument needs to be map, which is what I'm not intended to.
When I do like below, it doesn't complaining about map?. (although still a error because it's not valid at all)
(filter-ids {{:0 {:label "task0" :tags #{"one" "two"}}} 1
{:0 #{"three"}, :1 #{"one"}} 2}
nil)
I'm a newbie and really need some help to move on.
Thanks.

spec/cat is a "sequence regex" and it "unrolls" if you nest it inside another spec/cat.
You can either wrap the inner spec/cat call in a spec/spec call, which prevents that unrolling, or you can switch to spec/tuple (and remove the :items and :fields labels):
(spec/fdef filter-ids
:args (spec/cat
:useful (spec/spec (spec/cat
:items (spec/map-of :common/id :common/item)
:fields (spec/map-of :common/id :common/tags)))
:useless any?)
:ret (spec/coll-of :common/id :kind set?))
;; or
(spec/fdef filter-ids
:args (spec/cat
:useful (spec/tuple
(spec/map-of :common/id :common/item)
(spec/map-of :common/id :common/tags))
:useless any?)
:ret (spec/coll-of :common/id :kind set?))
Both of those will work. Which you choose may depend on what information you want in your error messages (I think the former provides more context when you get something wrong because of the :items and :fields labels).

Related

How do I edit Reitit routes in Reagent?

The routes created with the default reagent template look like this:
;; -------------------------
;; Routes
(def router
(reitit/router
[["/" :index]
["/items"
["" :items]
["/:item-id" :item]]
["/about" :about]]))
If I change the path of one ("/about" to "/test" below), why does it no longer work? There must be something else driving the routing, but I can't seem to figure out what.
;; -------------------------
;; Routes
(def router
(reitit/router
[["/" :index]
["/items"
["" :items]
["/:item-id" :item]]
["/test" :about]]))
This is the default reagent template (lein new reagent...) and I haven't changed anything else in the code. Any help would be greatly appreciated.
Edit - Some additional detail
I poked around in the repl a little bit in this function (from the default template):
(defn init! []
(clerk/initialize!)
(accountant/configure-navigation!
{:nav-handler
(fn [path]
(let [match (reitit/match-by-path router path)
current-page (:name (:data match))
route-params (:path-params match)]
(reagent/after-render clerk/after-render!)
(session/put! :route {:current-page (page-for current-page)
:route-params route-params})
(clerk/navigate-page! path)
))
:path-exists?
(fn [path]
(boolean (reitit/match-by-path router path)))})
(accountant/dispatch-current!)
(mount-root))
Everything looks ok to me. In fact, executing the below steps in the repl successfully navigated the browser to the correct page. I still can't enter the URL directly though.
app:problem.core=> (require '[reitit.frontend :as reitit])
nil
app:problem.core=> (reitit/match-by-path router "/test")
{:template "/test",
:data {:name :about},
:result nil,
:path-params {},
:path "/test",
:query-params {},
:parameters {:path {}, :query {}}}
app:problem.core=> (def match (reitit/match-by-path router "/test"))
#'problem.core/match
app:problem.core=> (:name (:data match))
:about
app:problem.core=> (:path-params match)
{}
app:problem.core=> (def current-page (:name (:data match)))
#'problem.core/current-page
app:problem.core=> (page-for current-page)
#'problem.core/about-page
app:problem.core=> (session/put! :route {:current-page (page-for current-page) :route-params {}})
{:route {:current-page #'problem.core/about-page, :route-params {}}}
app:problem.core=>
It looks like you changed the routes on client-side, in src/cljs/<project_name>/core.cljs, but did not change them server side in src/clj/<project_name>/handler.clj (look under the def app near the bottom of the file).
If your new to developing web applications with Clojure, I'd recommend looking at Luminus, rather than using the Reagent template. It's a much more batteries included-approach, with a lot more documentation. The book "Web Development With Clojure" is written by the same author (who is also a contributor to Reagent), and is also recommended reading.

Decode, in clojure, a JSON, clojure.data.json & cheshire.core, can't custom decode w/cheshire

My project parses JSONs, with a read/write library, called:
cheshire.core
I was having problems, trying to get the decode (func) to work, so I started messing around with:
data.json
My JSON contains data that consists of a field named "zone" this contains a vector with :keys inside, like so {:zone : [:hand :table]} that is stored into strings within the vector stored like so: {"zone" : ["hand" "table"]}
So I figured out how to convert the sample data using:
(mapv keyword {"zone" : ["hand"]})
which was great, I then needed to figure out how to implement a decoder for cheshire, I couldn't do this with my logic, I only spent like an hour working on this, but I had been using data.json, and the decoder function is relatively easy I think.
I got my project to work, here is some sample code:
(ns clojure-noob.core (:require
[cheshire.core :refer [decode]]
[clojure.data.json :as j-data]
) (:gen-class))
(defn -main
"I don't do a whole lot ... yet."
[& args]
)
this is using cheshire:
(let [init (decode "{\"zone\" : [\"hand\"]}" true
(fn [field-name]
(if (= field-name "zone")
(mapv keyword [])
[])))]
(println (str init)))
this is using data.json:
(defn my-value-reader [key value]
(if (= key :zone)
(mapv keyword value)
value))
(let [init (j-data/read-str
"{\"zone\" : [\"hand\"]}"
:value-fn my-value-reader
:key-fn keyword)]
(println (str init)))
I want the bottom result of these two from the console:
{:zone ["hand"]}
{:zone [:hand]}
The problem is I would like to do this using cheshire 😎
p.s. I am reading the factory section of cheshire? maybe this easier?
I would agree with #TaylorWood. Don't mess with the decoder, just do a bite in a time. First, parse json. Second, transform the result.
(def data "{\"zone\" : [\"hand\"]}")
(-> data
(cheshire.core/decode true)
(update-in ["zone"] (partial mapv keyword)))
#=> {:zone [:hand]}
I recommend you use a tool like schema.tools to coerce the input. You can add a second pass that attempts to coerce JSON strings into richer clojure types.
Here's some sample code!
;; require all the dependencies. See links below for libraries you need to add
(require '[cheshire.core :as json])
(require '[schema.core :as s])
(require '[schema.coerce :as sc])
(require '[schema-tools.core :as st])
;; your data (as before)
(def data "{\"zone\" : [\"hand\"]}")
;; a schema that wants an array of keywords
(s/defschema MyData {:zone [s/Keyword]})
;; use `select-schema` along with a JSON coercion matcher
(-> data
(json/decode true)
(st/select-schema MyData sc/json-coercion-matcher))
;; output: {:zone [:hand]}
Using defschema to define the shape of data you want gives you a general solution for serializing into JSON while getting the full benefit of Clojure's value types. Instead of explicitly "doing" the work of transforming, your schema describes the expected outcome, and hopefully coercions can do the right thing!
Links to libraries:
- https://github.com/plumatic/schema
- https://github.com/metosin/schema-tools#coercion
Note: you can do a similar thing with clojure.spec using metosin/spec-tools. Check out their readme for some help.

NPM Package fails to compile in Clojurescript

I am trying to use uber/react-map-gl with Clojurescript. I have added it the project.clj-
:npm-deps {"#cljs-oss/module-deps" "^1.1.1"
:react-map-gl "^3.3.0-alpha.5"
:react "^16.4.1"}
and npm installed it separately. When I require it from my ns -
(:require [react-map-gl :as M
:refer [BaseControl TRANSITION_EVENTS
FlyToInterpolator]])
(js/console.log M)
(js/console.log BaseControl)
(js/console.log M/BaseControl) ;;-> also fails
M is correctly logged to the console, but drilling into the attributes (such as BaseControl) throws an Exception -
Exception: ReferenceError:
...$node_modules$react_map_gl$dist$esm$components$base_control is not defined at Object.get BaseControl [as BaseControl] (http://localhost:3449/js/compiled/out/node_modules/react-map-gl/dist/esm/index.js:5:19) at Object.remoteFunction (<anonymous>:2:14)]
A handful of the attributes are accessible (e.g. no exceptions), such as TRANSITION_EVENTS and FlyToInterpolator.
I see this bug (which may or may not be relevant), but I am not sure how to proceed or even troubleshoot. Here's a minimal failing example to repro. Any help would be appreciated.
This is not an answer (really just a comment).
I was able to ostensibly get farther by first manually requiring all of the components and overlays (which points to some deps mismanagement of some sort).
$ clj -m cljs.main -co compile-opts.edn -r
ClojureScript 1.10.339
cljs.user=> (require '"react-map-gl/dist/esm/components/popup"
'"react-map-gl/dist/esm/components/navigation-control"
'"react-map-gl/dist/esm/components/interactive-map"
'"react-map-gl/dist/esm/components/base-control"
'"react-map-gl/dist/esm/components/static-map"
'"react-map-gl/dist/esm/components/marker"
'"react-map-gl/dist/esm/overlays/svg-overlay"
'"react-map-gl/dist/esm/overlays/html-overlay"
'"react-map-gl/dist/esm/overlays/canvas-overlay"
'[react-map-gl :as M
:refer [BaseControl TRANSITION_EVENTS
FlyToInterpolator]])
cljs.user=> M
#js {:default nil, :InteractiveMap nil, :StaticMap nil, :BaseControl nil, :Marker nil, :Popup nil, :NavigationControl nil, :CanvasOverlay nil, :HTMLOverlay nil, :SVGOverlay nil, :TRANSITION_EVENTS #js {:BREAK 1, :SNAP_TO_END 2, :IGNORE 3}, :TransitionInterpolator #object[TransitionInterpolator], :LinearInterpolator #object[LinearInterpolator], :FlyToInterpolator #object[ViewportFlyToInterpolator], :experimental #js {:MapControls #object[MapControls]}}
You can see that, while this avoids the ...$node_modules$react_map_gl$dist$esm$components$base_control is not defined error, it is not really a solution as things like :BaseControl end up being nil.
You can get a sense of the dependencies at play by making a revision to the compiler to log the results of calls to cljs.closure/index-node-modules.
My deps.edn:
{:deps {org.clojure/clojurescript {:mvn/version "1.10.339"}}}
and compile-opts.edn:
{:npm-deps {:react-map-gl "^3.3.0-alpha.5"
:react "^16.4.1"}
:install-deps true
:output-dir "out"}

Om Next read multi-fn not being called in second level join, Query AST not parsed fully, therefore component only receiving idents

I'm having trouble getting a second level join to work correctly. I've elided some things here for brevities sake.
My root component is:
(defui RootView
static om/IQuery
(query [this]
`[{:list/events ~(om/get-query Event)}])
Object
(render [this]
(let [{:keys [list/events]} (om/props this)]
(events/event-list events))))
My queries compose correctly and the initial data is normalised correctly. I won't show the normalised data and there's more to the total query.
(prn (om/get-query RootView)) =>
[{:list/events
[:id
{:body [:id :text :headline]}
{:media [:id :url :caption :credit]}
{:start-date [:id :year :month :day]}]}]
If I run a query containing the joins through a parser I get:
(prn (parser {:state (atom norm-data)}
'[{:list/events
[:id
{:body [:id :text :headline]}
{:media [:id :url :caption :credit]}
{:start-date [:id :year :month :day]}]}])) =>
{:list/events
[{:id 1,
:media [:media/by-id 1],
:start-date [:start-date/by-id 1],
:body [:body/by-id 1]}
{:id 17,
:media [:media/by-id 17],
:start-date [:start-date/by-id 17],
:body [:body/by-id 17]}]}
So the read function for :list/events is called and returns it's data, though all the second joins for :body, :media and :start-date are not.
My read functions are as follows, the second one is the one that is not called. I've left out the multi-methods on :media and :start-date, they also are not called. I'm not sure what this is a symptom of though.
(defmulti read om/dispatch)
(defmethod read :list/events
[{:keys [state] :as env} key params]
(let [st #state]
{:value (into [] (map #(get-in st %)) (get st key))}))
(defmethod read :body
[{:keys [state query]} key _]
(println "This is never printed")
{:value :doesnt-matter})
The join is correctly identified in the AST (so I assume the query grammar is correct) and the dispatch key matches that of the multi-method.
(prn (om/query->ast (om/get-query RootView))) =>
{:type :root,
:children
[{:type :join,
:dispatch-key :list/events,
:key :list/events,
:query
[:id
{:body [:id :text :headline]}
{:media [:id :url :caption :credit]}
{:start-date [:id :year :month :day]}],
:component timeline.components.events/Event,
:children
[{:type :prop, :dispatch-key :id, :key :id}
{:type :join,
:dispatch-key :body,
:key :body,
:query [:id :text :headline],
:component timeline.components.events/EventBody,
:children
[{:type :prop, :dispatch-key :id, :key :id}
{:type :prop, :dispatch-key :text, :key :text}
{:type :prop, :dispatch-key :headline, :key :headline}]}]}]}
I can't understand why the parser or something (?) stops at the second join? As far as my limited understanding goes, the multi-method on :body should at least be called?
So the issue I'm having is one of understanding I think, António Monteiro in the Om Slack channel suggested I use the db->tree function. Using this in the :list/events multi-method let's it return the whole tree of de-normalised data.
You have to do the recursion from within the reads yourself i.e. invoke the parser on the query that is within the key being examined. db->tree does this for you. In fact it is not unusual for every read to call db->tree and so look pretty much the same. In fact because of this Untangled does away with these reads altogether. In which case you really don't have to do the recursion yourself!
There's no recursion here:
(into [] (map #(get-in st %)) (get st key))
Any get on a key is to the refs part of the default db formatted data (app data). So here a sequence of idents will be returned by (get st key). Any get-in is to the tables part of the app data, and so returns real data values. (map #(get-in st %)) is the transducer that does this for every ident. But the tables part of the data is a recursive data structure - has to be for a lack of repetition - so any data that is not 'leaf' data is represented by an ident. So that's what you are getting back - anything that's one level deep and idents otherwise.
This answer is going to make next to no sense without an understanding of the default database format - the refs and tables parts. The best explanation I've found so far is here
This data (st) is in default db format:
{ :list/people [[:people/by-id 1] [:people/by-id 2] ... ]
:people/by-id { 1 { :db/id 1 :person/name "Joe" :person/mate [:people/by-id 2]}
2 { :db/id 2 :person/name "Sally" :person/mate [:people/by-id 1]}}}
Wherever you see by-id that's a give away that the key is in a tables mapentry. As you can see by the structure (get-in st [:people/by-id 1]) will retrieve for you a map that is the real data, of course only to one level deep.
Here :list/people is a key where the associated value is a vector of idents. (get st :list/people) will give you this vector. This mapentry is the refs part of st.

Serializing recursive refs in Datomic

I have a user entity type in my Datomic database which can follow other user types. My issue comes when one user follows another user who is already following them:
User A follows user B and also User B follows user A
When I try to serialize (using Cheshire) I get a StackOverflowError because of (I'm guessing) infinite recursion on the :user/follows-users attribute.
How would I go about serializing (to json for an API) two Datomic entities that reference each another in such a way?
Here's a basic schema:
; schema
[{:db/id #db/id[:db.part/db]
:db/ident :user/username
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity
:db.install/_attribute :db.part/db}
{:db/id #db/id[:db.part/db]
:db/ident :user/follows-users
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many
:db.install/_attribute :db.part/db}
; create users
{:db/id #db/id[:db.part/user -100000]
:user/username "Cheech"}
{:db/id #db/id[:db.part/user -200000]
:user/username "Chong"}
; create follow relationships
{:db/id #db/id[:db.part/user -100000]
:user/follows-users #db/id[:db.part/user -200000]}
{:db/id #db/id[:db.part/user -200000]
:user/follows-users #db/id[:db.part/user -100000]}]
And once the database is set up etc. on repl:
user=> (use '[cheshire.core :refer :all])
nil
user=> (generate-string (d/touch (d/entity (d/db conn) [:user/username "Cheech"])))
StackOverflowError clojure.lang.RestFn.invoke (RestFn.java:433)
The eager expansion of linked data structures is only safe in any language if they are cycle free. An api that promises to "eagerly expand data only until a cycle is found and then switch to linking (by user id)" may be harder to consume reliably than one that never expanded and always returned enough users to follow all the links in the response. For instance the request above could return the JSON:
[{"id": -100000,
"username": "Cheech",
"follows-users": [-200000]}
{"id": -200000,
"username": "Chong",
"follows-users": [-100000]}]
Where the list of selected users is found by reducing walk of the users graph into a set.
I'm a bit of a n00b to Datomic and am certain there must be a more idiomatic way of doing what #arthur-ulfeldt suggests above but in case anyone else is looking for a quick pointer on how to go about serializing Datomic EntityMaps into json where a self-referencing ref exists, here's the code that solves my problem:
(defn should-pack?
"Returns true if the attribute is type
ref with a cardinality of many"
[attr]
(->>
(d/q '[:find ?attr
:in $ ?attr
:where
[?attr :db/valueType ?type]
[?type :db/ident :db.type/ref]
[?attr :db/cardinality ?card]
[?card :db/ident :db.cardinality/many]]
(d/db CONN) attr)
first
empty?
not))
(defn make-serializable
"Stop infinite loops on recursive refs"
[entity]
(def ent (into {} entity))
(doseq [attr ent]
(if (should-pack? (first attr))
(def ent (assoc ent
(first attr)
(map #(get-entity-id %) (first (rest attr)))))))
ent)