ClojureScript, Figwheel, Devcards, Race Condition - clojure

Here is my minimal failure case.
(ns hello
(:require-macros [devcards.core :as dc])
(:require [reagent.core :as r]
[devcards.core :as dc]
[gamma.api :as g]
[gamma.program :as p]
[goog.dom :as gdom]
[goog.webgl :as ggl]))
(defn main []
(let [canvas (.getElementById js/document "webgl")
gl (.getContext canvas "webgl")] ;; *** THIS LINE ***
(.clearColor gl 0.0 0.0 0.0 1.0)
(.clear gl gl.COLOR_BUFFER_BIT)))
(dc/defcard-rg canvas-example
[:div
[:canvas {:width 600
:height 600
:id "webgl"}]])
(main)
Here's what happens when I load this up in figwheel/devcard.
First time loading page: "Cannot read property 'getContext' of null" on * THIS LINE *. This is because the devcard canvas hasn't been setup yet.
If I make a pointless change and save the file, the code reloads and works fine. This is because the devcard canvas HAS been setup.
It's clear this is a race condition between (a) when (main) runs and (b) when devcard's :canvas is setup.
How do I fix this? Ideally, I want to tag something to the canvas saying "run the main function after this ..."

Okay, I figured this out.
The simplest solution (i.e. does not involve hacking devcard / reagent) is to just have a separate cljs/go thread check every 50ms to see whether the element exists, and if so, exec the function.

Related

How to import local Java class at Clojure REPL?

There are existing answers to similar questions, but they tend to use Maven, which is not an option for my project. Also, I have not found any which give concrete examples of the syntax you use to import at the repl, especially when the class is local as opposed to from the web.
I want to import a Java class into my Clojure project:
public class MyLocalClass1 {
int x;
String y;
public MyLocalClass1() {
this.x = 0;
this.y = "hello there";
}
public MyLocalClass1(int x, String y) {
this.x = x;
this.y = y;
}
public void setX(int x) {
this.x = x;
}
public void setY(String y) {
this.y = y;
}
public int getX() {
return x;
}
public String getY() {
return y;
}
}
I successfully built it in Idea as Java2Import.jar.
Here is project.clj, where I import the jar:
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
:url "https://www.eclipse.org/legal/epl-2.0/"}
:dependencies [[org.clojure/clojure "1.10.1"]]
:resource-paths ["/path/to/my/Java/project/Java2Import/artifacts/Java2Import/"]
:repl-options {:init-ns import-local-java-4.core})
However, when I try to import, I get errors:
import-local-java-4.core=> (import (Java2Import.Java2Import))
Syntax error macroexpanding clojure.core/import at (/tmp/form-init9909516591129619328.clj:1:1).
Java2Import.Java2Import - failed: #{(quote quote)} at: [:class :quoted-spec :quote] spec: :clojure.core.specs.alpha/quotable-import-list
() - failed: Insufficient input at: [:package-list :spec :classes] spec: :clojure.core.specs.alpha/package-list
Java2Import.Java2Import - failed: #{(quote quote)} at: [:package-list :quoted-spec :quote] spec: :clojure.core.specs.alpha/quotable-import-list
(Java2Import.Java2Import) - failed: simple-symbol? at: [:class :spec] spec: :clojure.core.specs.alpha/quotable-import-list
What am I doing wrong?
Clone this repo and look at the directory structure and also project.clj
https://github.com/io-tupelo/clj-java-template
The source code files look like so:
~/io-tupelo/clj-java-template > ls -1 **/*.{clj,java}
project.clj
src/clj/demo/core.clj
src/java/demo/Calc.java
test/clj/_bootstrap.clj
test/clj/tst/demo/core.clj
The test namespace shows the correct syntax:
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test)
(:require
[tupelo.string :as str])
(:import [demo Calc]))
(dotest
(is= 5 (add2 2 3)) ; from src/clj/demo/core.clj
(is= 42 (Calc/add2 29 13)) ; from src/java/demo/Calc.java
)
Run the unit tests in test/clj/tst/demo/core.clj:
~/io-tupelo/clj-java-template > lein clean ; lein test
Compiling 1 source files to /Users/alanthompson/io-tupelo/clj-java-template/target/default+test+test/class-files
lein test _bootstrap
-----------------------------------
Clojure 1.10.3 Java 17.0.2
-----------------------------------
lein test tst.demo.core
Ran 2 tests containing 2 assertions.
0 failures, 0 errors.
You don't need to compile the Java code into a JAR file. In face, it is much easier if you just use the Java source files.
You can also run in the REPL:
~/io-tupelo/clj-java-template > lein repl
Compiling 1 source files to /Users/alanthompson/io-tupelo/clj-java-template/target/default/class-files
demo.core=> (import '[demo Calc])
demo.Calc
demo.core=> (prn :result (Calc/add2 6 7))
:result 13
nil
Please note that the function call syntax of require and import is different in the REPL than the macro syntax of the (ns ...) form!
Also, any edits to your Java code will not be picked up in the REPL. You will need to exit and restart the REPL to force a Java recompile.
P.S.
As the README explains, I find it even better to use the lein-test-refresh plugin. IMHO it is like a REPL on steroids!
P.P.S.
If you only have a JAR file (i.e. not Java source code), you may wish to use the lein install command, which will copy the JAR file into the local Maven cache ~/.m2 where Leiningen can find it as normal.

Shared env vars between profiles when using yogthos/config in clojure?

The https://github.com/yogthos/config approach let's you lay out per-profile env variables in separate files, like the below , in a project.clj .
Per the below, one can use lein with-profile prod uberjar or lein with-profile dev repl and the like.
But my issue is I have been unable to figure out how to place some common values into a shared area, accessible by dev, stage, prod profiles.
Basic example
(defproject edn-config-test "0.1.0-SNAPSHOT"
...
:profiles {:shared {:resource-paths ["config/shared"]}
:dev {:resource-paths ["config/dev"]}
:stage {:resource-paths ["config/stage"]}
:prod {:resource-paths ["config/prod"]}}
...
(with files)
config/shared/config.edn
config/dev/config.edn
config/stage/config.edn
config/prod/config.edn
I tried this without luck
lein with-profile shared,prod lein , borrowing from the composite approach in
https://github.com/technomancy/leiningen/blob/stable/doc/PROFILES.md#composite-profiles
When I do that, I only get variables in prod profile, for example.
I think it is a limitation of config. I tried this (more explicit):
:profiles {:dev {:resource-paths ["config/shared" "config/dev"]}
:prod {:resource-paths [ "config/prod" "config/shared"]}}
However, the last file wins and the first is ignored. So for :dev the shared stuff is ignored, and for :prod the prod stuff is ignored (like it doesn't exist):
config/dev/config.edn => {:special-val :dev-val}
config/prod/config.edn => {:special-val :prod-val}
cat config/shared/config.edn => {:shared-val 42}
and results:
> lein with-profile prod run
(:shared-val env) => 42
(:special-val env) => nil
> lein with-profile dev run
(:shared-val env) => nil
(:special-val env) => :dev-val
Perhaps you'd like to submit an enhancement PR to the project?
Here is the problem. It uses io/resource to read config.edn, which implicitly expects there to be only one file config.edn anywhere on the classpath:
(defn- read-config-file [f]
(try
(when-let [url (io/resource f)]
(with-open [r (-> url io/reader PushbackReader.)]
(edn/read r))) ...
(read-config-file "config.edn")
So you'd have to get away from the hard-coded filename config.edn, and make something like config-dev.edn, config-prod.edn, and config-shared.edn. At least then they could all live in a single ./resources dir.

How to show error on missing parameters for options?

I'm an absolute Clojure beginner and I'm trying to build a CLI app using the clojure.tools.cli library.
My problem is that I can't show any error when an option is not provided with required parameter.
What I want:
$ java -jar test.jar -m SAMPLE
Given file: SAMPLE
$ java -jar test.jar -m
ERROR: Please provide a file
What happens:
$ java -jar test.jar -m SAMPLE
Given file: SAMPLE
$ java -jar test.jar -m
$
It doesn't show anything.
Here is my code:
(ns obmed-clj.core
(:require [clojure.tools.cli :refer [parse-opts]])
(:gen-class))
(def cli-options
[["-m" "--menu FILE" "Provide menu file path"
:parse-fn #(if (nil? %)
(println "ERROR: Please provide a file")
%)
:validate-fn #(println "Given file:" %)]])
(defn -main [& args]
(parse-opts args cli-options))
You are abusing the -fn arguments here a little. Their use is to convert the "string" (in your case, since you have "--menu FILE") and then do additional validation on that (but rather use :validate [fn msg] instead). So e.g.:
user=> (def cli-opts [["-m" "--menu FILE" "menu file"
:parse-fn #(java.io.File. %)
:validate [#(.exists %) "file must exist"]]])
#'user/cli-opts
Missing argument:
user=> (parse-opts ["-m"] cli-opts)
{:arguments [],
:errors ["Missing required argument for \"-m FILE\""],
:options {},
:summary " -m, --menu FILE menu file"}
File not existing:
user=> (parse-opts ["-m" "XXX"] cli-opts)
{:arguments [],
:errors ["Failed to validate \"-m XXX\": file must exist"],
:options {},
:summary " -m, --menu FILE menu file"}
All is well:
user=> (parse-opts ["-m" "/etc/hosts"] cli-opts)
{:arguments [],
:errors nil,
:options {:menu #<java.io.File#34d63c80 /etc/hosts>},
:summary " -m, --menu FILE menu file"}

with-redefs-fn fails to pick up binding from do-seq?

There's something fundamental I'm not getting here. I expected the following test to pass. But the 2nd test case "staging"/"staging" fails. Its as if with-redefs-fn is failing to advance through the test-case instances. But the logging says everything is fine. This is confusing.
(deftest test-bad-derive-s3-environment
(testing "variants of props environments"
(doseq [test-case [{:env "qa1" :expect "qa1"}
{:env "dev" :expect "qa1"}
{:env "staging" :expect "staging"}]]
(log/infof "test-case %s" test-case)
(with-redefs-fn {#'config/environment (fn [] (:env test-case))}
(let [actual (fs/derive-s3-environment (config/environment))
_ (log/infof "within redefs :env %s :expect %s" (:env test-case) (:expect test-case))]
#(is (= actual (:expect test-case))))))))
...
lein test com.climate.test.mapbook.filestore
2016-05-03 16:16:29,353 INFO filestore:288 - test-case {:env "qa1", :expect "qa1"}
2016-05-03 16:16:29,355 INFO EnvConfig:98 - Loading config properties from /export/disk0/wb/etc/env.properties
2016-05-03 16:16:29,357 INFO EnvConfig:98 - Loading config properties from /export/disk0/wb/etc/local.properties
2016-05-03 16:16:29,358 INFO filestore:288 - within redefs :env qa1 :expect qa1
2016-05-03 16:16:29,359 INFO filestore:288 - test-case {:env "staging", :expect "staging"}
2016-05-03 16:16:29,359 INFO filestore:288 - within redefs :env staging :expect staging
lein test :only com.climate.test.mapbook.filestore/test-bad-derive-s3-environment
FAIL in (test-bad-derive-s3-environment) (filestore.clj:29)
variants of props environments
expected: (= actual (:expect test-case))
actual: (not (= "qa1" "staging"))
2016-05-03 16:16:29,364 INFO filestore:288 - test-case {:env "dev", :expect "qa1"}
2016-05-03 16:16:29,364 INFO filestore:288 - within redefs :env dev :expect qa1
Why does my with-redefs-fn fail to redefine the config/environment function in terms of the current test-case?
First of all, notice that your final test instance has an :expect of "qa1" – the same as the first test instance – so it should actually fail if the code worked as you intended it to; its passing is a symptom of the same problem as the second instance's failing.
Now for the fix – there are two options:
Just use with-redefs instead of with-redefs-fn:
(with-redefs [config/environment (fn [] (:env test-case))]
…)
Most of the time this is what you want to do and you can consider with-redefs-fn to be an implementation detail behind with-redefs – although strictly speaking it does have some utility of its own in that it can redefine dynamically constructed collections of Vars.
Use with-redefs-fn, but move the inner let form inside the anonymous function:
(with-redefs-fn {…}
#(let […]
(is …)))
Finally, the reason these work and the version from the question text does not:
with-redefs-fn is a function, so at runtime its arguments will be evaluated before it is actually invoked with their runtime values passed in. In particular, the let expression that you pass in as the second argument will be evaluated before the redefinition takes place, and so the local called actual will get the result of evaluating (config/environment) before the redefinition as its value, and that value will be installed in the anonymous closure created in the let's body. That closure, however, will then be called with the redefinition in place, and so it will take its notion of the "actual" value from before the redefinition and compare it with the expectation set after the redefinition, resulting in the observed behaviour.
Moving the let inside the closure, as in the second approach above, fixes this mismatch problem – the let local's value is computed with the redefinition in place and all is well. The first approach using with-redefs expands to the second approach.
The log printouts are fine, because they are only concerned with the doseq local and never examine any Vars. If they did, they would only see the pre-redefinition values.

Difference between lein repl (-main "something") and lein run "something")

Newbie Clojure and leiningen question:
Given the code snippet in my project below, this works from the lein repl :
==> (-main "something")
produces the expected "Command: something ... running ... done"
but doesn't work from the command line:
me pallet1]lein run "something"
produces "Command: something ... error: not resolved as a command"
Why? / how do I fix it?
To reproduce:
lein new eg
Then edit the generated project file, adding :main eg.core to define the main function, and edit the generated src/eg/core.clj file, and paste this in:
core.clj
(ns eg.core)
(defn something [] (println "Something!"))
(defn run-command-if-any [^String commandname]
(printf "Command: %s ..." commandname)
(if-let [cmd (ns-resolve *ns* (symbol commandname))]
(
(println "running ...") (cmd) (println "done.")
)
(println "error: not resolved as a command.")
))
(defn -main [ commandname ] (run-command-if-any commandname))
Then
lein repl
eg.core=> (-main "something")
works (ie prints "Something!) , but
lein run something
doesn't (ie prints the "error: not resolved" message)
The problem is that when you run it from lein your default namespace is "user" namespace:
(defn -main [ commandname ] (println *ns*))
Prints #<Namespace user>. So it doesn't contain something function because it is from another namespace. You have several choices:
Pass fully qualified function name: your-namespace/something instead of something.
Use your-namespace instead of *ns*: (ns-resolve 'your-namespace (symbol commandname))
Change namespace to your-namespace in -main.
Example of method 3:
(defn -main [ commandname ]
(in-ns 'your-namespace)
(run-command-if-any commandname))
Also you if you want to call several functions one by one you should use do:
(do (println "Hello")
(println "World"))
Not just braces like ( (println "hello") (println "World"))
the lein exec plugin is very useful for scripting such things in the context of a project. I have used this extensively for writing Jenkins jobs in clojure and other scripting situations
lein exec -pe '(something ...) (something-else) (save-results)'