commit 50e78e7f16dd4d5273653547ffd8ca1569d4b757 Author: Oly Date: Mon Nov 30 19:50:16 2020 +0000 Initial version. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24e1386 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/resources/public/cljs-out/ +/.nrepl-port +/.cpcache/ +dsl-demo/target/ +dsl-demo/.cpcache/ +reagent-reitit-demo/.cpcache/ +reagent-reitit-demo/resources/public/cljs-out/ +spec-demo/resources/public/cljs-out/ \ No newline at end of file diff --git a/datalog-demo/deps.edn b/datalog-demo/deps.edn new file mode 100644 index 0000000..cb06970 --- /dev/null +++ b/datalog-demo/deps.edn @@ -0,0 +1,5 @@ +{:deps {org.clojure/clojure {:mvn/version "1.10.0"} + org.clojure/clojurescript {:mvn/version "1.10.764"} + datascript {:mvn/version "1.0.0"} + com.bhauman/figwheel-main {:mvn/version "0.2.11"}} + :paths ["src" "resources"]} diff --git a/datalog-demo/dev.cljs.edn b/datalog-demo/dev.cljs.edn new file mode 100644 index 0000000..2fbd8f2 --- /dev/null +++ b/datalog-demo/dev.cljs.edn @@ -0,0 +1,7 @@ +{:output-to "resources/public/cljs-out/dev-main.js" + :optimizations :none + :pretty-print true + :source-map true + :source-map-timestamp true + :devcards true + :main core} diff --git a/datalog-demo/figwheel-main.edn b/datalog-demo/figwheel-main.edn new file mode 100644 index 0000000..f28aa9a --- /dev/null +++ b/datalog-demo/figwheel-main.edn @@ -0,0 +1,5 @@ +{ + :target-dir "resources" + :watch-dirs ["src"] +; :css-dirs ["resources/public/css"] +} diff --git a/datalog-demo/readme.org b/datalog-demo/readme.org new file mode 100644 index 0000000..ff26487 --- /dev/null +++ b/datalog-demo/readme.org @@ -0,0 +1,80 @@ +#+TITLE: Minimal clojurescript project demoing datalog queries + +* Getting started +To work with the code interactively jack into the project in your IDE of choice select "figwheel-main" as the build tool and dev as the build. + +Alternatively start a repl from the command line with + +#+BEGIN_SRC sh +clojure -m figwheel.main --build dev --repl +#+END_SRC + +* Intro to datalog + +See the extensive comments in the src code for a working example, also watch / read these for a good intro. + +https://www.youtube.com/watch?v=oo-7mN9WXTw +http://www.learndatalogtoday.org/ +https://udayv.com/clojurescript/clojure/2016/04/28/datascript101/ + +Blog of the dev who makes datascript +https://tonsky.me/blog/the-web-after-tomorrow/ + + +** Creating a DATABASE +Datalog databases can be schema less but a lot of the power comes from creating a schema specifying uniqueness and relations. + +*** Schema less Database +In it's simplest for we create a database connection like below. +#+BEGIN_SRC clojurescript :tangle ./src/test.cljs + (def demo-conn (d/create-conn {})) +#+END_SRC + +Using the connection we can just start inserting data, using standard hash maps and lists structures, we always specify the attribute and the value when transacting. +#+BEGIN_SRC clojurescript :tangle ./src/test.cljs + (d/transact! demo-conn [{:user/name "Oly" :user/img "me.jpg"} {:user/name "Sam" :user/img "you.jpg"}]) +#+END_SRC + +*** Using a Schema +In this example we are saying name is unique and rooms has a many to one relationship, when we transact data will be inserted even if its not in the schema but rules stop things like duplicates from happening. +#+BEGIN_SRC clojurescript :tangle ./src/test.cljs + (def schema {:user/name {:db/unique :db.unique/identity} + :user/rooms {:db/cardinality :db.cardinality/many + :db/valueType :db.type/ref}}) + (def demo-conn (d/create-conn schema)) +#+END_SRC + +Transacting this data would mean Oly would be inserted once but the image will be updated to =you.jpg= +#+BEGIN_SRC clojurescript :tangle ./src/test.cljc + (d/transact! demo-conn [{:user/name "Oly" :user/img "me.jpg"} {:user/name "Oly" :user/img "you.jpg"}]) +#+END_SRC + +** Querying the databases +There are three types of queries in datalog entity lookup's pulling a tree of data or querying with =d/q=. + +*** Looking up an entity +=d/entity= is used to find the entity id, using any unique piece of data for example the user =Oly= exists once so the entity db/id will be returned which can be used for further queries. +#+BEGIN_SRC clojurescript :tangle ./src/test.cljc +(d/entity @conn [:user/name "Oly"]) +#+END_SRC + +*** Pull a tree of data +Pull is used with entity id's once you know the entity you can specify what data you want to view ='[*]= being the most common looking up all keys, you can also specify the attributes your interested in looking up including there relations to make a more specific view. +#+BEGIN_SRC clojurescript :tangle ./src/test.cljc +(d/pull @demo-conn '[*] 1) +(d/pull @demo-conn '[:user/name :user/rooms] 1) +#+END_SRC + +*** Querying your dataset +Querying in datalog is all about binding variables to your entities attributes and values which you can use in you conditions or to return in the result set. + +In this example we return the user-id and user/name in the find clause which we looked up in the where clause by finding all attributes =:user/name= the binding the entity id and username to variables on each match to display in the find clause. + +#+BEGIN_SRC clojurescript :tangle ./src/test.cljc + (d/q '[:find ?user-entity ?user-name :where + [?user-entity :user/name ?user-name]] @demo-conn) +#+END_SRC + + + +Write some more as needed better examples in the src code. diff --git a/datalog-demo/resources/public/rename-me-index.html b/datalog-demo/resources/public/rename-me-index.html new file mode 100644 index 0000000..ef36acc --- /dev/null +++ b/datalog-demo/resources/public/rename-me-index.html @@ -0,0 +1,30 @@ + + + + + + + + Atom Juice Merchant Website + + + + + + + + + +
+ loading here +
+ + + + diff --git a/datalog-demo/src/core.cljs b/datalog-demo/src/core.cljs new file mode 100644 index 0000000..2c9622f --- /dev/null +++ b/datalog-demo/src/core.cljs @@ -0,0 +1,99 @@ +(ns core.demo + (:require [datascript.core :as d])) + +;; schema is not required you can use {} but it allows you to create relations and unique attributes in your database +;; some info on schema options here https://docs.datomic.com/on-prem/schema.html +(def schema + "Database schema layout, applies constraints to certain fields" + ;; only want the username to exist once + {:user/name {:db/unique :db.unique/identity} + ;;our user can belong to many rooms + :user/rooms {:db/cardinality :db.cardinality/many + :db/valueType :db.type/ref} + :room/name {:db/unique :db.unique/identity}}) + +;; connection to database + + +(def conn (d/create-conn schema)) + + +;; notice how users room is a vector of maps, if :room/name does not exist its created +;; if it does exists it links via the entity ids, so in this example oly's :user/rooms stores the ids #{7 8} + + +(def initial-data-payload + [{:user/name "Oly" :user/rooms [{:room/name "room1"} {:room/name "room2"}]} + {:user/name "Sam" :user/rooms [:room/name "room1"]} + {:user/name "Brett" :user/rooms [:room/name "room1"]} + {:user/name "Cameron"} + {:user/name "Matt"} + {:user/name "Zoran"} + {:room/name "room1" :room/link "room1-link" :room/description "room description"} + {:room/name "room2" :room/link "room2-link" :room/description "room description"} + {:room/name "room3" :room/link "room3-link" :room/description "room description"}]) + +(d/transact! conn initial-data-payload) + +;; dealing with entities these are basically unique identifies like ids or primary keys from sql +;; you can look up entities by id or by a unique attribute set in the schema +;; entities return a single item like in a hash map or dictionary +(d/entity @conn 1) +(d/entity @conn [:user/name "Oly"]) + +;; often we want a lot more details this is where pull comes into play +;; '[*] is a shorthand to pull anything, including relations we can also specify the attributes +(d/pull @conn '[*] 1) +(d/pull @conn '[:user/name :user/rooms] 1) + +;; to query data from the database you use d/q and a few notations +;; in the where notice everything is a triple ie an [entity attribute value] +;; if we don't care about something in the eav we use an _ to ignnore +;; we can create variables with ?var-name on any of the triplet + + +;; find all rooms and there unique entity ids, try doing the same for user perhaps +(d/q '[:find ?room-name2 + :where [_ :room/name ?room-name2]] @conn) + + +;; in this example we say find all entities containing the value "room1" in the :user/room attribute +;; bind the entity to a value called ?user-entity +;; in the second where clause we use the ?user-entity which now has a value to find any attributes with :user/name matching the same entity id and bind the result to ?users variable +;; the find then pulls the result from ?users we could add in ?user-entity if we needed it or any other attribute wee wish to pull + + +(d/q '[:find ?users :where + [?user-entity :user/rooms "room1"] + [?user-entity :user/name ?users]] @conn) + +;; find room1 entity id, then find all users entities joined to the room entity then find the usernames of the users joined to the room +(d/q '[:find ?room-entity ?user-entity ?user :where + [?room-entity :room/name "room1"] + [?user-entity :user/rooms ?room-entity] + [?user-entity :user/name ?user]] @conn) + +;; we could ignore the join data and get a list of all users combined to room1 +(d/q '[:find ?room-entity ?room-name ?user :where + [?room-entity :room/name "room1"] + [?room-entity :room/name ?room-name] + [?user-entity :user/name ?user]] @conn) + + +;; we can use pull inside a query, this will find room1 and pull all data with the same entity +(d/q '[:find (pull ?room-entity [*]) :where + [?room-entity :room/name "room1"]] @conn) + + +;; aggregates, how many rooms do we have "." is short hand so we dont get a nested Vector +;; try with out + + +(d/q '[:find (count ?room-entity) . :where + [?room-entity :room/name _]] @conn) + +;; we could so a sum instead, summing the entities is a bit silly but you get the idea +(d/q '[:find (sum ?room-entity) . :where + [?room-entity :room/name _]] @conn) + + diff --git a/dsl-demo/deps.edn b/dsl-demo/deps.edn new file mode 100644 index 0000000..c49b878 --- /dev/null +++ b/dsl-demo/deps.edn @@ -0,0 +1,11 @@ +{:deps {org.clojure/clojure {:mvn/version "1.10.0"} + org.clojure/clojurescript {:mvn/version "1.10.764"} + org.clojure/test.check {:mvn/version "0.10.0"} + com.bhauman/figwheel-main {:mvn/version "0.2.11"} + + honeysql {:mvn/version "0.9.8"} + nilenso/honeysql-postgres {:mvn/version "0.2.6"} + + hiccup {:mvn/version "2.0.0-alpha2"} + } + :paths ["src" "resources"]} diff --git a/dsl-demo/project.clj b/dsl-demo/project.clj new file mode 100644 index 0000000..f4b07cd --- /dev/null +++ b/dsl-demo/project.clj @@ -0,0 +1,8 @@ +(defproject dsl-demos "0.1.0-SNAPSHOT" + :description "Demo" + :plugins [[lein-tools-deps "0.4.5"] [lein-ring "0.12.5"]] + :uberjar-name "dsl-demos.jar" + :middleware [lein-tools-deps.plugin/resolve-dependencies-with-deps-edn] + :lein-tools-deps/config {:config-files [:install :user :project]} + :repl-options {:init-ns beanbag.core} + :main dsl.core) diff --git a/dsl-demo/readme.org b/dsl-demo/readme.org new file mode 100644 index 0000000..1e7af17 --- /dev/null +++ b/dsl-demo/readme.org @@ -0,0 +1,244 @@ +#+TITLE: Intro to HoneySQL & Hiccup + + +* Hiccup +https://github.com/weavejester/hiccup +hiccup is a html DSL, used extensively in the clojure's eco-system, there are others as well but hiccup is the most widely used. + +In hiccup everything is a list, this means you can easily compose html using standard language constructs. + +** Simple examples +If using reagent you don't need to pass into h/html but this is server side reagent also has some helpers to work with react nicer. +#+BEGIN_SRC clojure +(h/html [:span "bar"]) +#+END_SRC + +Attributes are added as a map of values styles are also a map +#+BEGIN_SRC clojure +(h/html [:span "bar" {:class "class1 class2" :title "my title" :style {:color "red"}}]) +#+END_SRC + +You can use shorthand to add id's and classes +#+BEGIN_SRC clojure +(h/html [:span#id.class1.class2 "bar"]) +#+END_SRC + +** Compossible components +The main advantage comes from the ability to compose the parts together, so we can break this into function's + +In this example its only a hash map, the data is separated out from the html, in reagent you can use atom for live updating. + + +#+BEGIN_SRC clojure +(defn navbar-link [{:keys [href title text] :or {text nil title nil}}] + [:a.link.dim.white.dib.mr3 {:href href :title title} text]) + +(defn navbar [links] + [:header.bg-black-90.fixed.w-100.ph3.pv3.pv4-ns.ph4-m.ph5-l + [:nav.f6.fw6.ttu.tracked + (map navbar-link links)]]) + +(h/html (navbar [{:href "link1" :title "title here"} + {:href "link2" :title nil} + {:href "link3" :text "link text"} + {:href "link4"}])) +#+END_SRC + +You can use clojure core language to manipulate these lists. + +place parts inside another containing element +#+BEGIN_SRC clojure +(h/html (into [:div.container] [[:span "span 1"] [:span "span 2"]])) +#+END_SRC + +You could also use merge +#+BEGIN_SRC clojure +(h/html (merge [:span "span 1"] [:span "span 2"] [:span "span 3"])) +#+END_SRC + +We can take advantage of lazyness if we like +#+BEGIN_SRC clojure +(h/html (take 2 (map navbar-link [{:href "link1" :title "title here"} + {:href "link2" :title nil} + {:href "link3" :text "link text"} + {:href "link4"}]))) +#+END_SRC + + +* HoneySQL +https://github.com/seancorfield/honeysql +HoneySQL is a DSL specifically for building SQL queries, much like hiccup you build up a data structure that can be composed, honey has a lot of helper functions available so you don't need to build these maps manually. + +Honey does not care about connecting to your database it only builds your queries. + +A nice way to write these is using =->= which is a threading macro, put simply the result one call is passed to the next. +** Basic queries +first tip is sql/format will convert to an sql query, use it to examine your query's or run else where. +In this example we create a select but with no from format will still give you a query but it will be incomplete. +#+BEGIN_SRC clojure +(sql/format (sqlh/select :first_name :last_name :email)) +#+END_SRC + +You use multiple functions to combine the parts, below we add the missing from. +#+BEGIN_SRC clojure +(sql/format (sqlh/from + (sqlh/select :first_name :last_name :email) + :users)) +#+END_SRC + + +How ever it's much nicer for readability to use the threading macro +#+BEGIN_SRC clojure +(sql/format + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users))) +#+END_SRC + +The functions understand ordering so your order does not matter. +#+BEGIN_SRC clojure +(sql/format + (-> (sqlh/from :users) + (sqlh/select :first_name :last_name :email))) +#+END_SRC + + +We can do the usual things like limiting & ordering etc +#+BEGIN_SRC clojure +(sql/format + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users) + (sqlh/order-by :first_name) + (sqlh/limit 10))) +#+END_SRC + + +** Filtering + +Filtering is just as simple and support the usual < > not in type expressions. +#+BEGIN_SRC clojure +(sql/format + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users) + (sqlh/where [:= :first_name "spot"] + [:= :last_name "dog"]))) +#+END_SRC + +Often we want to conditionally filter, this is nice and simple with the knowledge that where will short circuit give nil. + +So below no where will not be appended because true is not false so the when return nil which removes the where in the final query. +#+BEGIN_SRC clojure +(sql/format + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users) + (sqlh/where (when (true? false) [:= :first_name "spot"])))) +#+END_SRC + + +** Extending / joins +For all the standard fn's like select and where there are equivalent merge fn's the merge versions append in place of replacing. + +A good strategy is to build basic queries extending them when needed. + +we can use =if= =when= =when-let= =cond->= to help build these. + +#+BEGIN_SRC clojure +(def base-sql + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users))) + +(defn user-search [{:keys [first-name last-name] :or {first-name nil last-name nil}}] + (sql/format + (-> base-sql + (sqlh/select :first_name :last_name) + (sqlh/merge-where (when first-name [:= :first_name first-name])) + (sqlh/merge-where (when last-name [:= :last_name last-name]))))) + +(sql/format (user-search {:first-name "spot"})) +#+END_SRC + +Now is a good time to explain aliasing, basically keywords become vectors so :first_name would become [:first_name :fn] to alias the column first_name to fn we can aliases columns tables sub select the lot like in standard sql. +#+BEGIN_SRC clojure +(sql/format + (-> base-sql + (select [:first_name :fn] [:last_name :ln] [:email :e]))) +#+END_SRC + + +We can also do joins to other table's +#+BEGIN_SRC clojure + (def base-sql + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users))) + + (def base-join-sql + (-> base-sql + (sqlh/join [:address] [:= :users.address_id address.id]))) + +(sql/format base-join-sql) +#+END_SRC + +or group by's and sql functions like =count= =max= =min= these can be used by appending :%name to the selected column. + +#+BEGIN_SRC clojure +(def base-group-sql + (-> base-sql + (sqlh/select :first_name [:%count.first_name :count_name]) + (sqlh/group :first_name))) + +(sql/format base-group-sql) +#+END_SRC + +** Larger query +This is how I like to compose queries, and shows a larger query being generated. +#+BEGIN_SRC clojure +(def big-base-sql + (-> (sqlh/select :users.* :address.* :products.*) + (sqlh/from :users) + (sqlh/join :address [:= :users.address_id :address.id]) + (sqlh/join :products [:= :users.address_id :address.id]) + (sqlh/limit 100))) + +(defn big-base-filters [filters] + (-> big-base-sql + (sqlh/merge-where + (when (:first_name filters) + [:= :first_name (:first_name filters)])) + (sqlh/merge-where + (when (:last_name filters) + [:= :last_name (:last_name filters)])) + (sqlh/merge-where + (when (:product_name filters) + [:= :product.name (:product_name filters)])) + (sqlh/merge-where + (when (:active filters) + [:= :active (:active filters)])))) + +(sql/format + (big-base-filters + {:first_name "spot" + :last_name "dog" + :product_name "lead" + :active true})) +#+END_SRC + +Don't forget its just data, if you don't use sql/format it just returns a data structure which you can build manually, or manipulate with the standard library. + +#+BEGIN_EXAMPLE + {:select (:first_name :last_name :email), :from (:users)} +#+END_EXAMPLE + +** Extending / raw sql + +When all else fails you have a few options, check to see if there is a honeysql db specific library or break out =sql/raw= or extending honey sql. + +Say we want to get people added in the last 14 days this is a bit more tricky +#+BEGIN_SRC clojure +(def base-last-14-days-sql + (-> base-sql + (sqlh/where [:> + (sql/raw "created") + (sql/raw "CURRENT_DATE - INTERVAL '14' DAY")]))) + + +(sql/format base-last-14-days-sql) +#+END_SRC diff --git a/dsl-demo/src/core.clj b/dsl-demo/src/core.clj new file mode 100644 index 0000000..3665124 --- /dev/null +++ b/dsl-demo/src/core.clj @@ -0,0 +1,178 @@ +(ns core + (:require [hiccup.core :as h] + [honeysql.core :as sql] + [honeysql.helpers :refer :all :as sqlh])) + + +;; If using reagent you don't need to pass into h/html but this is server side +;; reagent also has some helpers to work with react nicer + + +(h/html [:span "bar"]) + + +;; attributes are added as a map of values +;; styles are also a map + + +(h/html [:span "bar" {:class "class1 class2" :title "my title" :style {:color "red"}}]) + +;; you can shorthand to add id's and classes +(h/html [:span#id.class1.class2 "bar"]) + +;; A more complex example building a nav bar +(h/html [:header.bg-black-90.fixed.w-100.ph3.pv3.pv4-ns.ph4-m.ph5-l + [:nav.f6.fw6.ttu.tracked + [:a.link.dim.white.dib.mr3 {:href "#" :title "Home"} "Home"] + [:a.link.dim.white.dib.mr3 {:href "#" :title "About"} "About"] + [:a.link.dim.white.dib.mr3 {:href "#" :title "Store"} "Store"] + [:a.link.dim.white.dib {:href "#" :title "Contact"} "Contact"]]]) + +;; Obviously the main advantage comes from composing, so we can break this into function's +(defn navbar-link [{:keys [href title text] :or {text nil title nil}}] + [:a.link.dim.white.dib.mr3 {:href href :title title} text]) + +(defn navbar [links] + [:header.bg-black-90.fixed.w-100.ph3.pv3.pv4-ns.ph4-m.ph5-l + [:nav.f6.fw6.ttu.tracked + (map navbar-link links)]]) + +;; now its only a hash map and no html, in reagent you can use atom for live updating +;; notice how nil removes the attribute's so its easy to make something optional +(h/html (navbar [{:href "link1" :title "title here"} + {:href "link2" :title nil} + {:href "link3" :text "link text"} + {:href "link4"}])) + + +;; place parts inside another containing element + + +(h/html (into [:div.container] [[:span "span 1"] [:span "span 2"]])) +;; you could also use merge +(h/html (merge [:span "span 1"] [:span "span 2"] [:span "span 3"])) + +;; we can take advantage of lazyness if we like +(h/html (take 2 (map navbar-link [{:href "link1" :title "title here"} + {:href "link2" :title nil} + {:href "link3" :text "link text"} + {:href "link4"}]))) + + +;; first tip is sql/format will convert to an sql query, use it to split things up +;; in this example we create a select but with no from format will still give you a query +;; when you run the query you will get an error + + +(sql/format (sqlh/select :first_name :last_name :email)) + +;; we can join fn's together to add data +;; in this example we add the from +(sql/format (sqlh/from + (sqlh/select :first_name :last_name :email) + :users)) + +;; how ever it's much nicer to use the threading macro +(sql/format + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users))) + +;; ordering does not matter other than for your sanity :-) +(sql/format + (-> (sqlh/from :users) + (sqlh/select :first_name :last_name :email))) + +;; we can do our usual things like limiting ordering etc +(sql/format + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users) + (sqlh/order-by :first_name) + (sqlh/limit 10))) + +(sql/format + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users) + (sqlh/where [:= :first_name "spot"] + [:= :last_name "dog"]))) + +;; honey gives us a range of merge functions, below base-sql is a simple query +;; user-search extend the query replacing the selected column's and appends the where clauses if values are supplied, we could also use merge-select to extend the previous select. +;; we can use =if= =when= =when-let= =cond->= to help build these. +(def base-sql + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users))) + +(defn user-search [{:keys [first-name last-name] :or {first-name nil last-name nil}}] + (sql/format + (-> base-sql + (sqlh/select :first_name :last_name) + (sqlh/merge-where (when first-name [:= :first_name first-name])) + (sqlh/merge-where (when last-name [:= :last_name last-name]))))) + +(sql/format (user-search {:first-name "spot"})) + +;; now is a good time to explain aliasing, basically keywords become vectors so :first_name would become [:first_name ] +(sql/format + (-> base-sql + (select [:first_name :fn] [:last_name :ln] [:email :e]))) + +;; extending base above we can add in an inner join statement +;; we can also alias the joined table name +(def base-join-sql + (-> base-sql + (sqlh/join [:address :a] [:= :users.address_id :a.id]))) + +(sql/format base-join-sql) + + +;; grouping by name count occurrences + + +(def base-group-sql + (-> base-sql + (sqlh/select :first_name [:%count.first_name :count_name]) + (sqlh/group :first_name))) + +(sql/format base-group-sql) + +;; when all else fails you have a few options, break out =sql/raw= or extending honey sql +;; say we want to get people added in the last 14 days this is a bit more tricky +(def base-last-14-days-sql + (-> base-sql + (sqlh/where [:> + (sql/raw "created") + (sql/raw "CURRENT_DATE - INTERVAL '14' DAY")]))) + +(sql/format base-last-14-days-sql) + +(def big-base-sql + (-> (sqlh/select :users.* :address.* :products.*) + (sqlh/from :users) + (sqlh/join :address [:= :users.address_id :address.id]) + (sqlh/merge-join :products [:= :users.address_id :address.id]) + (sqlh/limit 100))) + +(defn big-base-filters [filters] + (-> big-base-sql + (sqlh/merge-where + (when (:first_name filters) + [:= :first_name (:first_name filters)])) + (sqlh/merge-where + (when (:last_name filters) + [:= :last_name (:last_name filters)])) + (sqlh/merge-where + (when (:product_name filters) + [:= :product.name (:product_name filters)])) + (sqlh/merge-where + (when (:active filters) + [:= :active (:active filters)])))) + +(sql/format + (big-base-filters + {:first_name "spot" + :last_name "dog" + :product_name "lead" + :active true})) + + + diff --git a/dsl-demo/src/cors.cljs b/dsl-demo/src/cors.cljs new file mode 100644 index 0000000..87d14ac --- /dev/null +++ b/dsl-demo/src/cors.cljs @@ -0,0 +1,7 @@ +(ns dsl.core + (:require [hiccup.core :as h] + [honeysql.core :as sql] + [honeysql.helpers :refer :all :as helpers]) + ) + +(html [:span {:class "foo"} "bar"]) diff --git a/getting-started/readme.org b/getting-started/readme.org new file mode 100644 index 0000000..f71b30c --- /dev/null +++ b/getting-started/readme.org @@ -0,0 +1,33 @@ + +Step 1 +Install =lein= from the url below check that the =lein= command works from the command line. +https://leiningen.org/ + +Step 2 +Install clojure and make sure =clj= and =clojure= command work from the command line. +https://clojure.org/guides/getting_started + +Step 3 +Make sure you have java installed, even if not using java and are targeting javascript some of the tools use this eco system. + +Step 4 +Download an IDE visual studio with calva plugin is recommended and very capable CIDER is also very popular but has a much steeper learning curve if your not familiar with emacs I don't recommend learning both at the same time. +https://github.com/BetterThanTomorrow/calva + +Step 5 +Check you can jack in to a simple project. + +#+BEGIN_SRC sh +git clone https://github.com/bhauman/flappy-bird-demo-new.git +#+END_SRC + +Try running it at the command line first if that works then try connecting. + +#+BEGIN_SRC sh +clj -A:build +#+END_SRC + +Then try connecting from your IDE + +As an example. +https://www.youtube.com/watch?v=a2vRDYXDAug diff --git a/reagent-reitit-demo/deps.edn b/reagent-reitit-demo/deps.edn new file mode 100644 index 0000000..9575ac9 --- /dev/null +++ b/reagent-reitit-demo/deps.edn @@ -0,0 +1,10 @@ +{:deps {org.clojure/clojure {:mvn/version "1.10.0"} + org.clojure/clojurescript {:mvn/version "1.10.764"} + cljs-ajax {:mvn/version "0.8.1"} + reagent {:mvn/version "0.9.1"} + reagent-utils {:mvn/version "0.3.3"} + metosin/reitit {:mvn/version "0.5.10"} + metosin/reitit-spec {:mvn/version "0.5.10"} + metosin/reitit-frontend {:mvn/version "0.5.10"} + com.bhauman/figwheel-main {:mvn/version "0.2.11"}} + :paths ["src" "resources"]} diff --git a/reagent-reitit-demo/dev.cljs.edn b/reagent-reitit-demo/dev.cljs.edn new file mode 100644 index 0000000..6798d2b --- /dev/null +++ b/reagent-reitit-demo/dev.cljs.edn @@ -0,0 +1,8 @@ +^{:watch-dirs ["src"]} +{:output-to "resources/public/cljs-out/dev-main.js" + :optimizations :none + :pretty-print true + :source-map true + :source-map-timestamp true + :devcards true + :main core.demo} diff --git a/reagent-reitit-demo/figwheel-main.edn b/reagent-reitit-demo/figwheel-main.edn new file mode 100644 index 0000000..f28aa9a --- /dev/null +++ b/reagent-reitit-demo/figwheel-main.edn @@ -0,0 +1,5 @@ +{ + :target-dir "resources" + :watch-dirs ["src"] +; :css-dirs ["resources/public/css"] +} diff --git a/reagent-reitit-demo/readme.org b/reagent-reitit-demo/readme.org new file mode 100644 index 0000000..4f99506 --- /dev/null +++ b/reagent-reitit-demo/readme.org @@ -0,0 +1,118 @@ +#+TITLE: Minimal clojurescript reagent reitit example + + +https://github.com/reagent-project/reagent + +https://purelyfunctional.tv/guide/reagent/#what-is-reagent + +https://github.com/metosin/reitit + +https://www.metosin.fi/blog/reitit/ + +* Figwheel Main +Figwheel is a tool that does hot reloading of your code, it greatly simplifys the tooling required to develop and deploy your applications. + +https://figwheel.org/ + +https://figwheel.org/docs/hot_reloading.html + +* Using this demo +Run at the shell with the below command, alternatively in your ide jack in using figwheel-main and the dev build when asked by your ide. +#+BEGIN_SRC sh +clj -m figwheel.main --build dev --repl +#+END_SRC + +* reload hooks +You can control how reloading works by using meta data in your project, usually adding it as a tag on your main namespace, and hooking your main function which calls =reagent/render= see below for example hooks. + +#+BEGIN_SRC clojurescript +(ns ^:figwheel-hooks core.demo) +#+END_SRC + +#+BEGIN_SRC clojurescript +(defn ^:after-load render-site []) +#+END_SRC + +* Components +Reagent allow multiple ways to create components with increasing complexity. +There is some good info in this article. +https://purelyfunctional.tv/guide/reagent/ + +** Form 1 components +Form one components are for simply rendering some html with values that are not going to change. + +In the example below the function just returns some hiccup with the parameters inserted, you need to specify =:key= when dynamically repeating the elements and these should be reproducible unique id's where possible not randomly generated or indexed numbers if the data is unordered. +#+BEGIN_SRC clojurescript +(defn navbar-link [{:keys [href title text] :or {text nil title nil}}] + [:a.link.dim.white.dib.mr3 {:key href :href href :title title} text]) +#+END_SRC + + +** Form 2 components +In form two we can track local state inside a component a click counter being a basic example. + + +#+BEGIN_SRC clojurescript + +#+END_SRC + + +** Form 3 components +This form of component give's you full access to the react life cycle methods, so render did-mount did-unmount etc +usually this form of component is only needed when rendering graphics or things like graphs, it's also useful for capturing errors and handling them as in the example below, which renders your components but if =component-did-catch= is trigger the error is caught and displayed instead. +#+BEGIN_SRC clojurescript +(defn err-boundary + [& children] + (let [err-state (reagent/atom nil)] + (reagent/create-class + {:display-name "ErrBoundary" + :component-did-catch (fn [err info] + (reset! err-state [err info])) + :reagent-render (fn [& children] + (if (nil? @err-state) + (into [:<>] children) + (let [[_ info] @err-state] + [:pre [:code (pr-str info)]])))}))) +#+END_SRC + + +* Routing +Routing with reitit is all about data, you store your routes as nested vectors of hash maps. +the hash map should take a name and view param at least but you can add in params and validate the data. + +Reitit works as a backend and frontend routing library so you can share routes between the two. + +These are a few simple routes, the last takes parameters and does validation checking against the values. +#+BEGIN_SRC clojurescript +(def routes + [["/" + {:name ::frontpage + :view home-page-function}] + + ["/about" + {:name ::about + :view about-page-function}] + + ["/item/:id" + {:name ::item + :view item-page-function + :parameters {:path {:id int?} + :query {(ds/opt :foo) keyword?}}}]]) +#+END_SRC + +You need to connect your routes data structure to =ref/start!= this function take's your own function where you can handle what should happen on route change, in this example an atom is updated causing react to render the new page. + +#+BEGIN_SRC clojurescript + (rfe/start! + (rf/router routes {:data {:coercion rss/coercion}}) + (fn [m] (swap! site-state assoc :current-route m)) + ;; set to false to enable HistoryAPI + {:use-fragment true}) +#+END_SRC + + +To create a link to a route, you can use the =rfe/href= function which takes a lookup key which you specified in your routes, in this instance the key is name spaced to the current namespace. +#+BEGIN_SRC clojurescript + [:a {:href (rfe/href ::frontpage)} "example link"] + +#+END_SRC diff --git a/reagent-reitit-demo/resources/public/datalog-demo.org b/reagent-reitit-demo/resources/public/datalog-demo.org new file mode 100644 index 0000000..ff26487 --- /dev/null +++ b/reagent-reitit-demo/resources/public/datalog-demo.org @@ -0,0 +1,80 @@ +#+TITLE: Minimal clojurescript project demoing datalog queries + +* Getting started +To work with the code interactively jack into the project in your IDE of choice select "figwheel-main" as the build tool and dev as the build. + +Alternatively start a repl from the command line with + +#+BEGIN_SRC sh +clojure -m figwheel.main --build dev --repl +#+END_SRC + +* Intro to datalog + +See the extensive comments in the src code for a working example, also watch / read these for a good intro. + +https://www.youtube.com/watch?v=oo-7mN9WXTw +http://www.learndatalogtoday.org/ +https://udayv.com/clojurescript/clojure/2016/04/28/datascript101/ + +Blog of the dev who makes datascript +https://tonsky.me/blog/the-web-after-tomorrow/ + + +** Creating a DATABASE +Datalog databases can be schema less but a lot of the power comes from creating a schema specifying uniqueness and relations. + +*** Schema less Database +In it's simplest for we create a database connection like below. +#+BEGIN_SRC clojurescript :tangle ./src/test.cljs + (def demo-conn (d/create-conn {})) +#+END_SRC + +Using the connection we can just start inserting data, using standard hash maps and lists structures, we always specify the attribute and the value when transacting. +#+BEGIN_SRC clojurescript :tangle ./src/test.cljs + (d/transact! demo-conn [{:user/name "Oly" :user/img "me.jpg"} {:user/name "Sam" :user/img "you.jpg"}]) +#+END_SRC + +*** Using a Schema +In this example we are saying name is unique and rooms has a many to one relationship, when we transact data will be inserted even if its not in the schema but rules stop things like duplicates from happening. +#+BEGIN_SRC clojurescript :tangle ./src/test.cljs + (def schema {:user/name {:db/unique :db.unique/identity} + :user/rooms {:db/cardinality :db.cardinality/many + :db/valueType :db.type/ref}}) + (def demo-conn (d/create-conn schema)) +#+END_SRC + +Transacting this data would mean Oly would be inserted once but the image will be updated to =you.jpg= +#+BEGIN_SRC clojurescript :tangle ./src/test.cljc + (d/transact! demo-conn [{:user/name "Oly" :user/img "me.jpg"} {:user/name "Oly" :user/img "you.jpg"}]) +#+END_SRC + +** Querying the databases +There are three types of queries in datalog entity lookup's pulling a tree of data or querying with =d/q=. + +*** Looking up an entity +=d/entity= is used to find the entity id, using any unique piece of data for example the user =Oly= exists once so the entity db/id will be returned which can be used for further queries. +#+BEGIN_SRC clojurescript :tangle ./src/test.cljc +(d/entity @conn [:user/name "Oly"]) +#+END_SRC + +*** Pull a tree of data +Pull is used with entity id's once you know the entity you can specify what data you want to view ='[*]= being the most common looking up all keys, you can also specify the attributes your interested in looking up including there relations to make a more specific view. +#+BEGIN_SRC clojurescript :tangle ./src/test.cljc +(d/pull @demo-conn '[*] 1) +(d/pull @demo-conn '[:user/name :user/rooms] 1) +#+END_SRC + +*** Querying your dataset +Querying in datalog is all about binding variables to your entities attributes and values which you can use in you conditions or to return in the result set. + +In this example we return the user-id and user/name in the find clause which we looked up in the where clause by finding all attributes =:user/name= the binding the entity id and username to variables on each match to display in the find clause. + +#+BEGIN_SRC clojurescript :tangle ./src/test.cljc + (d/q '[:find ?user-entity ?user-name :where + [?user-entity :user/name ?user-name]] @demo-conn) +#+END_SRC + + + +Write some more as needed better examples in the src code. diff --git a/reagent-reitit-demo/resources/public/dsl-demo.org b/reagent-reitit-demo/resources/public/dsl-demo.org new file mode 100644 index 0000000..1e7af17 --- /dev/null +++ b/reagent-reitit-demo/resources/public/dsl-demo.org @@ -0,0 +1,244 @@ +#+TITLE: Intro to HoneySQL & Hiccup + + +* Hiccup +https://github.com/weavejester/hiccup +hiccup is a html DSL, used extensively in the clojure's eco-system, there are others as well but hiccup is the most widely used. + +In hiccup everything is a list, this means you can easily compose html using standard language constructs. + +** Simple examples +If using reagent you don't need to pass into h/html but this is server side reagent also has some helpers to work with react nicer. +#+BEGIN_SRC clojure +(h/html [:span "bar"]) +#+END_SRC + +Attributes are added as a map of values styles are also a map +#+BEGIN_SRC clojure +(h/html [:span "bar" {:class "class1 class2" :title "my title" :style {:color "red"}}]) +#+END_SRC + +You can use shorthand to add id's and classes +#+BEGIN_SRC clojure +(h/html [:span#id.class1.class2 "bar"]) +#+END_SRC + +** Compossible components +The main advantage comes from the ability to compose the parts together, so we can break this into function's + +In this example its only a hash map, the data is separated out from the html, in reagent you can use atom for live updating. + + +#+BEGIN_SRC clojure +(defn navbar-link [{:keys [href title text] :or {text nil title nil}}] + [:a.link.dim.white.dib.mr3 {:href href :title title} text]) + +(defn navbar [links] + [:header.bg-black-90.fixed.w-100.ph3.pv3.pv4-ns.ph4-m.ph5-l + [:nav.f6.fw6.ttu.tracked + (map navbar-link links)]]) + +(h/html (navbar [{:href "link1" :title "title here"} + {:href "link2" :title nil} + {:href "link3" :text "link text"} + {:href "link4"}])) +#+END_SRC + +You can use clojure core language to manipulate these lists. + +place parts inside another containing element +#+BEGIN_SRC clojure +(h/html (into [:div.container] [[:span "span 1"] [:span "span 2"]])) +#+END_SRC + +You could also use merge +#+BEGIN_SRC clojure +(h/html (merge [:span "span 1"] [:span "span 2"] [:span "span 3"])) +#+END_SRC + +We can take advantage of lazyness if we like +#+BEGIN_SRC clojure +(h/html (take 2 (map navbar-link [{:href "link1" :title "title here"} + {:href "link2" :title nil} + {:href "link3" :text "link text"} + {:href "link4"}]))) +#+END_SRC + + +* HoneySQL +https://github.com/seancorfield/honeysql +HoneySQL is a DSL specifically for building SQL queries, much like hiccup you build up a data structure that can be composed, honey has a lot of helper functions available so you don't need to build these maps manually. + +Honey does not care about connecting to your database it only builds your queries. + +A nice way to write these is using =->= which is a threading macro, put simply the result one call is passed to the next. +** Basic queries +first tip is sql/format will convert to an sql query, use it to examine your query's or run else where. +In this example we create a select but with no from format will still give you a query but it will be incomplete. +#+BEGIN_SRC clojure +(sql/format (sqlh/select :first_name :last_name :email)) +#+END_SRC + +You use multiple functions to combine the parts, below we add the missing from. +#+BEGIN_SRC clojure +(sql/format (sqlh/from + (sqlh/select :first_name :last_name :email) + :users)) +#+END_SRC + + +How ever it's much nicer for readability to use the threading macro +#+BEGIN_SRC clojure +(sql/format + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users))) +#+END_SRC + +The functions understand ordering so your order does not matter. +#+BEGIN_SRC clojure +(sql/format + (-> (sqlh/from :users) + (sqlh/select :first_name :last_name :email))) +#+END_SRC + + +We can do the usual things like limiting & ordering etc +#+BEGIN_SRC clojure +(sql/format + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users) + (sqlh/order-by :first_name) + (sqlh/limit 10))) +#+END_SRC + + +** Filtering + +Filtering is just as simple and support the usual < > not in type expressions. +#+BEGIN_SRC clojure +(sql/format + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users) + (sqlh/where [:= :first_name "spot"] + [:= :last_name "dog"]))) +#+END_SRC + +Often we want to conditionally filter, this is nice and simple with the knowledge that where will short circuit give nil. + +So below no where will not be appended because true is not false so the when return nil which removes the where in the final query. +#+BEGIN_SRC clojure +(sql/format + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users) + (sqlh/where (when (true? false) [:= :first_name "spot"])))) +#+END_SRC + + +** Extending / joins +For all the standard fn's like select and where there are equivalent merge fn's the merge versions append in place of replacing. + +A good strategy is to build basic queries extending them when needed. + +we can use =if= =when= =when-let= =cond->= to help build these. + +#+BEGIN_SRC clojure +(def base-sql + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users))) + +(defn user-search [{:keys [first-name last-name] :or {first-name nil last-name nil}}] + (sql/format + (-> base-sql + (sqlh/select :first_name :last_name) + (sqlh/merge-where (when first-name [:= :first_name first-name])) + (sqlh/merge-where (when last-name [:= :last_name last-name]))))) + +(sql/format (user-search {:first-name "spot"})) +#+END_SRC + +Now is a good time to explain aliasing, basically keywords become vectors so :first_name would become [:first_name :fn] to alias the column first_name to fn we can aliases columns tables sub select the lot like in standard sql. +#+BEGIN_SRC clojure +(sql/format + (-> base-sql + (select [:first_name :fn] [:last_name :ln] [:email :e]))) +#+END_SRC + + +We can also do joins to other table's +#+BEGIN_SRC clojure + (def base-sql + (-> (sqlh/select :first_name :last_name :email) + (sqlh/from :users))) + + (def base-join-sql + (-> base-sql + (sqlh/join [:address] [:= :users.address_id address.id]))) + +(sql/format base-join-sql) +#+END_SRC + +or group by's and sql functions like =count= =max= =min= these can be used by appending :%name to the selected column. + +#+BEGIN_SRC clojure +(def base-group-sql + (-> base-sql + (sqlh/select :first_name [:%count.first_name :count_name]) + (sqlh/group :first_name))) + +(sql/format base-group-sql) +#+END_SRC + +** Larger query +This is how I like to compose queries, and shows a larger query being generated. +#+BEGIN_SRC clojure +(def big-base-sql + (-> (sqlh/select :users.* :address.* :products.*) + (sqlh/from :users) + (sqlh/join :address [:= :users.address_id :address.id]) + (sqlh/join :products [:= :users.address_id :address.id]) + (sqlh/limit 100))) + +(defn big-base-filters [filters] + (-> big-base-sql + (sqlh/merge-where + (when (:first_name filters) + [:= :first_name (:first_name filters)])) + (sqlh/merge-where + (when (:last_name filters) + [:= :last_name (:last_name filters)])) + (sqlh/merge-where + (when (:product_name filters) + [:= :product.name (:product_name filters)])) + (sqlh/merge-where + (when (:active filters) + [:= :active (:active filters)])))) + +(sql/format + (big-base-filters + {:first_name "spot" + :last_name "dog" + :product_name "lead" + :active true})) +#+END_SRC + +Don't forget its just data, if you don't use sql/format it just returns a data structure which you can build manually, or manipulate with the standard library. + +#+BEGIN_EXAMPLE + {:select (:first_name :last_name :email), :from (:users)} +#+END_EXAMPLE + +** Extending / raw sql + +When all else fails you have a few options, check to see if there is a honeysql db specific library or break out =sql/raw= or extending honey sql. + +Say we want to get people added in the last 14 days this is a bit more tricky +#+BEGIN_SRC clojure +(def base-last-14-days-sql + (-> base-sql + (sqlh/where [:> + (sql/raw "created") + (sql/raw "CURRENT_DATE - INTERVAL '14' DAY")]))) + + +(sql/format base-last-14-days-sql) +#+END_SRC diff --git a/reagent-reitit-demo/resources/public/reagent-reitit.org b/reagent-reitit-demo/resources/public/reagent-reitit.org new file mode 100644 index 0000000..1f7137a --- /dev/null +++ b/reagent-reitit-demo/resources/public/reagent-reitit.org @@ -0,0 +1,112 @@ +#+TITLE: Minimal clojurescript reagent reitit example + + +https://github.com/reagent-project/reagent + +https://purelyfunctional.tv/guide/reagent/#what-is-reagent + +https://github.com/metosin/reitit + +https://www.metosin.fi/blog/reitit/ + +* Figwheel Main +https://figwheel.org/ + +https://figwheel.org/docs/hot_reloading.html + +Figwheel is a tool that does hot reloading of your code, it greatly simplifys the tooling required to develop and deploy your applications. + +You can control how reloading works by using meta data in your project, usually adding it as a tag on your main namespace, and hooking your main function which calls =reagent/render= + + +#+BEGIN_SRC clojurescript +(ns ^:figwheel-hooks core.demo) +#+END_SRC + +#+BEGIN_SRC clojurescript +(defn ^:after-load render-site []) +#+END_SRC + +* Components +Reagent allow multiple ways to create components with increasing complexity. +There is some good info in this article. +https://purelyfunctional.tv/guide/reagent/ + +** Form 1 components +Form one components are for simply rendering some html with values that are not going to change. + +In the example below the function just returns some hiccup with the parameters inserted, you need to specify =:key= when dynamically repeating the elements and these should be reproducible unique id's where possible not randomly generated or indexed numbers if the data is unordered. +#+BEGIN_SRC clojurescript +(defn navbar-link [{:keys [href title text] :or {text nil title nil}}] + [:a.link.dim.white.dib.mr3 {:key href :href href :title title} text]) +#+END_SRC + + +** Form 2 components +In form two we can track local state inside a component a click counter being a basic example. + + +#+BEGIN_SRC clojurescript + +#+END_SRC + + +** Form 3 components +This form of component give's you full access to the react life cycle methods, so render did-mount did-unmount etc +usually this form of component is only needed when rendering graphics or things like graphs, it's also useful for capturing errors and handling them as in the example below, which renders your components but if =component-did-catch= is trigger the error is caught and displayed instead. +#+BEGIN_SRC clojurescript +(defn err-boundary + [& children] + (let [err-state (reagent/atom nil)] + (reagent/create-class + {:display-name "ErrBoundary" + :component-did-catch (fn [err info] + (reset! err-state [err info])) + :reagent-render (fn [& children] + (if (nil? @err-state) + (into [:<>] children) + (let [[_ info] @err-state] + [:pre [:code (pr-str info)]])))}))) +#+END_SRC + + +* Routing +Routing with reitit is all about data, you store your routes as nested vectors of hash maps. +the hash map should take a name and view param at least but you can add in params and validate the data. + +Reitit works as a backend and frontend routing library so you can share routes between the two. + +These are a few simple routes, the last takes parameters and does validation checking against the values. +#+BEGIN_SRC clojurescript +(def routes + [["/" + {:name ::frontpage + :view home-page-function}] + + ["/about" + {:name ::about + :view about-page-function}] + + ["/item/:id" + {:name ::item + :view item-page-function + :parameters {:path {:id int?} + :query {(ds/opt :foo) keyword?}}}]]) +#+END_SRC + +You need to connect your routes data structure to =ref/start!= this function take's your own function where you can handle what should happen on route change, in this example an atom is updated causing react to render the new page. + +#+BEGIN_SRC clojurescript + (rfe/start! + (rf/router routes {:data {:coercion rss/coercion}}) + (fn [m] (swap! site-state assoc :current-route m)) + ;; set to false to enable HistoryAPI + {:use-fragment true}) +#+END_SRC + + +To create a link to a route, you can use the =rfe/href= function which takes a lookup key which you specified in your routes, in this instance the key is name spaced to the current namespace. +#+BEGIN_SRC clojurescript + [:a {:href (rfe/href ::frontpage)} "example link"] + +#+END_SRC diff --git a/reagent-reitit-demo/resources/public/rename-me-index.html b/reagent-reitit-demo/resources/public/rename-me-index.html new file mode 100644 index 0000000..ef36acc --- /dev/null +++ b/reagent-reitit-demo/resources/public/rename-me-index.html @@ -0,0 +1,30 @@ + + + + + + + + Atom Juice Merchant Website + + + + + + + + + +
+ loading here +
+ + + + diff --git a/reagent-reitit-demo/resources/public/test.org b/reagent-reitit-demo/resources/public/test.org new file mode 100644 index 0000000..a05442a --- /dev/null +++ b/reagent-reitit-demo/resources/public/test.org @@ -0,0 +1,3 @@ +#+title: title + +lorem diff --git a/reagent-reitit-demo/src/core.cljs b/reagent-reitit-demo/src/core.cljs new file mode 100644 index 0000000..44612a2 --- /dev/null +++ b/reagent-reitit-demo/src/core.cljs @@ -0,0 +1,200 @@ +(ns ^:figwheel-hooks core.demo + (:require [reagent.core :as reagent] + [ajax.core :refer [GET raw-response-format]] + [demo.org :refer [parse->to-hiccup parse-flat]] + [reitit.frontend :as rf] + [reitit.frontend.easy :as rfe] + [reitit.coercion.spec :as rss] + [spec-tools.data-spec :as ds])) + +;; put constant data here +(def site-data + {:demos {:dsl-demo + {:file "dsl-demo.org" :git-link "https://github.com/atomjuice/dsl-demo"} + :datalog-demo + {:file "datalog-demo.org" :git-link "https://github.com/atomjuice/dsl-demo"} + :reagent-demo + {:file "reagent-reitit.org" :git-link "https://github.com/atomjuice/dsl-demo"} + } + + :lorem "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."}) + +;; Store site state +(defonce site-state (reagent/atom {})) + +;; for one component to render an article +(defn article [{:keys [title description tagline]}] + [:article {:data-name "article-full-bleed-background"} + [:div.cf {:style {:background "url(http://mrmrs.github.io/photos/12.jpg)" + :no-repeat "center center fixed" :background-size "cover"}} + [:div.fl.pa3.pa4-ns.bg-white.black-70.measure-narrow.f3.times + [:header.b--black-70.pv4 {:class (when tagline "bb")} + [:h3.f2.fw7.ttu.tracked.lh-title.mt0.mb3.avenir title] + (when tagline [:h4.f3.fw4.i.lh-title.mt0 tagline])] + [:section.pt5.pb4 [:p.times.lh-copy.measure.f4.mt0 description]]]]]) + +;; form one component to render article tiles +(defn articles [{:keys [title articles]}] + [:section.mw7.center.avenir {:key title} + [:h2.baskerville.fw1.ph3.ph0-l title] + (map (fn [{:keys [title author link description img-src img-alt]}] + [:article.bt.bb.b--black-10 + [:a.db.pv4.ph3.ph0-l.no-underline.black.dim {:href link} + [:div.flex.flex-column.flex-row-ns + (when img-src + [:div.pr3-ns.mb4.mb0-ns.w-100.w-40-ns + [:img.db {:src img-src :alt img-alt}]]) + [:div.w-100.w-60-ns.pl3-ns + [:h1.f3.fw1.baskerville.mt0.lh-title title] + [:p.f6.f5-l.lh-copy description] + [:p.f6.lh-copy.mv0 author]]]]]) + articles)]) + +;; form one component to render a product +(defn product-card [{:keys [title amount description link]}] + [:article.br2.ba.dark-gray.b--black-10.mv4.w-100.w-50-m.w-25-l.mw5.center + [:img.db.w-100.br2.br--top {:src link}] + [:div.pa2.ph3-ns.pb3-ns + [:div.dt.w-100.mt1 + [:div.dtc [:h1.f5.f4-ns.mv0 title]] + [:div.dtc.tr [:h2.f5.mv0 amount]]] + [:p.f6.lh-copy.measure.mt2.mid-gray description]]]) + +(defn circle [{:keys [img alt]}] + [:div.pa4.tc [:img.br-100.ba.h3.w3.dib {:src img :alt alt}]]) + + +;; form one component ro render a nav link +(defn navbar-link [{:keys [href title text] :or {text nil title nil}}] + [:a.link.dim.white.dib.mr3 {:key href :href href :title title} text]) + +;; form one component to render a navbar +(defn navbar [links] + [:header.bg-black-90.w-100.ph3.pv3.pv4-ns.ph4-m.ph5-l + [:nav.f6.fw6.ttu.tracked + (map navbar-link links)]]) + +(defn my-component [title] + (let [local-state (reagent/atom true)] + (fn [] + [:h1 {:class (when @local-state "hid") :on-click (fn [] (swap! local-state not))} title]))) + + +;; form one homepage component + + +(defn home-page [] + [:<> + [my-component "component 1"] + [my-component "component 2"] + [circle {:alt "test"}] + [:p (:lorem @site-state)] + [articles {:title "demos" + :articles [{:title "DSL Demo" + :link (rfe/href ::demo {:page "dsl-demo"}) + :img-src "https://miro.medium.com/max/1400/1*CEYFj5R57UFyCXts2nsBqA.png"} + {:title "Datalog Demo" + :link (rfe/href ::demo {:page "datalog-demo"}) + :img-src "https://raw.githubusercontent.com/tonsky/datascript/master/extras/logo.svg"} + {:title "Reagent Demo" + :link (rfe/href ::demo {:page "reagent-demo"}) + :img-src "https://raw.githubusercontent.com/reagent-project/reagent/master/logo/logo-text.png"}]}]]) + +;; form one render demo component +(defn demo-page [route] + (let [demo-key (keyword (-> route :parameters :path :page)) + content (reagent/atom {})] + (GET (-> site-data :demos demo-key :file) + {:response-format (raw-response-format) + :handler (fn [response] + (prn response) + (prn (parse->to-hiccup response)) + (prn (parse-flat response)) + (reset! content (parse->to-hiccup response)))}) + (fn [route] + ;[:div (-> route :parameters :path :page)] + [:main.mt4 + [:div.mw7.center.avenir @content] + #_[article {:title "Homepage" + :description @content + :tagline "tagline here"}]]))) + +;; form one render about page component +(defn about-page [] + [:div "about" + [product-card {:title "title" :amount "10.59" :description "long description here" :href "test-link"}]]) + +;; form 3 component wrap rendering to catch errors and render them +;; or just render the rest of the page if all is good +;; this uses =create-class= and a hash map of life cycle functions +(defn err-boundary + [& children] + (let [err-state (reagent/atom nil)] + (reagent/create-class + {:display-name "ErrBoundary" + :component-did-catch (fn [err info] + (reset! err-state [err info])) + :reagent-render (fn [& children] + (if (nil? @err-state) + (into [:<>] children) + (let [[_ info] @err-state] + [:pre [:code (pr-str info)]])))}))) + +;; define our routes, just nested vectors of the route definition hash maps +(def routes + [["/" + {:name ::frontpage + :my-data "hi" + :view home-page}] + + ["/about" + {:name ::about + :view about-page}] + + ["/demo/:page" + {:name ::demo + :view demo-page + :parameters {:path {:page string?} + :query {(ds/opt :foo) keyword?}}}]]) + +;; top level component contains nav and adds in the select page into a containing element +;; we are adding in a style sheet but this will often be done in index.html +(defn current-page [] + [:<> [:link {:rel "stylesheet" :href "https://unpkg.com/tachyons@4.12.0/css/tachyons.min.css"}] + [navbar [{:href (rfe/href ::frontpage) :title "title here" :text "home"} + {:href (rfe/href ::about) :text "About"} + {:href (rfe/href ::i-do-not-exist) :text "missing"}]] + [:main.mt4 + (when-let [view (-> @site-state :current-route :data :view)] [view (-> @site-state :current-route)])]]) + + +;; This simply calls reagent render and puts the result in a div with the id of app +;; you can create your own index.html or figwheel provides one with the app id which will replace the default data +;; ^:after-load is meta data its not needed but informs figwheel to run this code after a page load + + +(defn mount-root-page [] + ;; this select the main node from the html file and injects your page content + (reagent/render + (fn [] [err-boundary [current-page]]) + (.getElementById js/document "app"))) + +(defn ^:after-load render-site [] + ;; this select the main node from the html file and injects your page content + (mount-root-page)) + +(defn startup! [] + (rfe/start! + (rf/router routes {:data {:coercion rss/coercion}}) + (fn [m] (swap! site-state assoc :current-route m)) + ;; set to false to enable HistoryAPI + {:use-fragment true}) + (render-site)) + +;; we defonce the startup so that hot reloading does not reinitialize the state of the site +(def launch (do (startup!) true)) + +(comment + @site-state + + (GET "/test.org" {:handler (fn [response] (swap! site-state assoc :content response))})) diff --git a/reagent-reitit-demo/src/org.cljs b/reagent-reitit-demo/src/org.cljs new file mode 100644 index 0000000..951fcd7 --- /dev/null +++ b/reagent-reitit-demo/src/org.cljs @@ -0,0 +1,162 @@ +(ns demo.org + (:require [clojure.string :as str])) + + +(def ESCAPE "\n") + +(def BLANK 0) +(def META 1) +(def META_OTHER 2) +(def HEADER 5) +(def BOLD 10) +(def ITALIC 11) +(def UNDERLINED 12) +(def VERBATIM 13) +(def LIST 20) +(def TEXT 21) +(def IMAGE 22) +(def LINK 23) +(def CAPTION 24) +(def BULLET 25) +(def SOURCE 50) +(def EXAMPLE 51) +(def RESULTS 52) +(def COMMENT 53) +(def TABLE 54) + +(def METADATA ["TITLE" + "AUTHOR" + "EMAIL" + "DESCRIPTION" + "KEYWORDS" + "FILETAGS" + "DATE" + "HTML_DOCTYPE" + "SETUPFILE"]) + +(def t_META #"^[#]\+(?:TITLE|AUTHOR|EMAIL|DESCRIPTION|KEYWORDS|FILETAGS|DATE|HTML_DOCTYPE|SETUPFILE)\:") +(def t_BLANK_LINE #"^\s*$") +(def t_COMMENT_BEGIN #"^\#\+BEGIN_COMMENT") +(def t_COMMENT_END #"^\#\+END_COMMENT") +(def t_EXAMPLE_BEGIN #"^\#\+BEGIN_EXAMPLE") +(def t_EXAMPLE_END #"^\#\+END_EXAMPLE") +(def t_SRC_BEGIN #"^\#\+BEGIN_SRC\s+") +(def t_SRC_END #"^\#\+END_SRC") +(def t_TABLE_START #"^\s*\|") +(def t_TABLE_END #"^(?!\s*\|).*$") +(def t_RESULTS_START #"^\#\+RESULTS\:") +(def t_CAPTIONS #"^\#\+CAPTION:") +(def t_NAME #"^\#\+NAME:") +; t_IMG #"^\[\[(\w|\.|-|_|/)+\]\]$" +(def t_IMG #"^\[\[") +(def t_IMG_END #"\]\]") +(def t_RESULTS_END #"^\s*$") +(def t_END_LABELS #"^(?!\[|\#).*") +(def t_BULLET_START #"^\s*[\+|\-|0-9\.]") +(def t_BULLET_END #"^(?!\s*[\+|\-|0-9\.]).*$") + +(def t_HEADER #"^\*+") +(def t_META_OTHER #"^[#]\+[A-Z\_]+\:") + +(def TYPE_SINGLE 0) +(def TYPE_BLOCK 1) +(def TYPE_ATTRIBUTE 2) + +(defn TokenStruct [values] + (merge {:start "" :end false :type TYPE_SINGLE :start_pos 2 :end_pos nil :count 0 :key ""} values)) + +(def token-map {:token nil :value nil :attrs nil}) + +(defn token + ([token-type value] (token token-type value nil)) + ([token-type value attrs] + {:token token-type :value (or value "") :attrs attrs})) + +(def TOKENS [[:META (TokenStruct {:start t_META, :end_pos -1 :key-fn #(clojure.string/join (drop 2 (butlast %)))})], + [:COMMENT (TokenStruct {:start t_COMMENT_BEGIN, :end t_COMMENT_END, :type TYPE_BLOCK, :end_pos -1})], + [:EXAMPLE (TokenStruct {:start t_EXAMPLE_BEGIN, :end t_EXAMPLE_END, :type TYPE_BLOCK, :end_pos -})], + [:IMAGE (TokenStruct {:start t_IMG, :end_pos -2})], + [:CAPTION (TokenStruct {:start t_CAPTIONS, :type TYPE_ATTRIBUTE, :key "caption"})], + [:br (TokenStruct {:start t_BLANK_LINE, :end_pos -1})], + [:SOURCE (TokenStruct {:start t_SRC_BEGIN, :end t_SRC_END})], + [:TABLE (TokenStruct {:start t_TABLE_START, :end t_TABLE_END, :start_pos 0})], + [:BULLET (TokenStruct {:start t_BULLET_START, :end t_BULLET_END, :start_pos 0})], + [:RESULTS (TokenStruct {:start t_RESULTS_START, :end t_RESULTS_END})], + [:HEADER (TokenStruct {:start t_HEADER, :start_pos 1, :count 0})], + [:META_OTHER (TokenStruct {:start t_META_OTHER, :start_pos 2, :end_pos -1})]]) + + +(def rm-ns (comp keyword name)) + +(defn mk-key [k t pos] + (case (name k) + "HEADER" (keyword (str (name k) pos)) + (rm-ns k))) + +(clojure.string/join (drop 2 (butlast "#+title:"))) +(re-find t_META "#+TITLE: abc12345def") +(re-find t_HEADER "** test") +(str/split "#+TITLE: abc12345def" t_META) +(def t + "#+TITLE: abc12345def +#+DESCRIPTION: test descriptioon + +* title +paragraph +text +** header test +hi there +") + +(defn re-split [rx line] + (let [matches (re-find rx line) + pos (if (string? matches) + (count matches) + (count (or (str (first matches)) matches))) + remainder (subs line pos)] + [matches pos remainder])) + +(defn match-token [line] + (->> TOKENS + (reduce (fn [m [k v]] + (let [s (re-find (:start v) line) + pos (if (string? s) (count s) (count (or (str (first s)) s))) + v (subs line pos)] + (when (not (nil? s)) + (reduced (remove nil? [(mk-key k v pos) (if (= "" v) nil v)]))))) []) + (#(or % [:p line])))) + + +(defn parse-line [text] + (loop [line (str/split-lines text) + r []] + (if (empty? line) + r + (recur (next line) + (if (= :HEADER1 (first (last r))) + (conj (last r) (match-token (first line))) + (conj r (match-token (first line)))))))) + +(defn parse-flat [text] + (->> (str/split-lines text) + (map (fn [a] (match-token a))))) + +(parse-line t ) +(parse-flat t ) + +(defn to->html [dsl] + (map #(apply conj [(case (first %) + :TITLE :h1 + :HEADER1 :h1 + :HEADER2 :h2 + :br :br + :SOURCE :pre + (first %))] (rest %)) dsl)) + +(defn parse->to-hiccup [org-document] + (to->html (parse-flat org-document))) + +(parse->to-hiccup t) +(parse->to-hiccup "") + +(map #(clojure.set/rename-keys (parse-line t) {:HEADER :h1}) (parse-line t )) diff --git a/spec-demo/deps.edn b/spec-demo/deps.edn new file mode 100644 index 0000000..c565706 --- /dev/null +++ b/spec-demo/deps.edn @@ -0,0 +1,5 @@ +{:deps {org.clojure/clojure {:mvn/version "1.10.0"} + org.clojure/clojurescript {:mvn/version "1.10.764"} + org.clojure/test.check {:mvn/version "0.10.0"} + com.bhauman/figwheel-main {:mvn/version "0.2.11"}} + :paths ["src" "resources"]} diff --git a/spec-demo/dev.cljs.edn b/spec-demo/dev.cljs.edn new file mode 100644 index 0000000..2fbd8f2 --- /dev/null +++ b/spec-demo/dev.cljs.edn @@ -0,0 +1,7 @@ +{:output-to "resources/public/cljs-out/dev-main.js" + :optimizations :none + :pretty-print true + :source-map true + :source-map-timestamp true + :devcards true + :main core} diff --git a/spec-demo/figwheel-main.edn b/spec-demo/figwheel-main.edn new file mode 100644 index 0000000..f28aa9a --- /dev/null +++ b/spec-demo/figwheel-main.edn @@ -0,0 +1,5 @@ +{ + :target-dir "resources" + :watch-dirs ["src"] +; :css-dirs ["resources/public/css"] +} diff --git a/spec-demo/readme.org b/spec-demo/readme.org new file mode 100644 index 0000000..c49d1e8 --- /dev/null +++ b/spec-demo/readme.org @@ -0,0 +1,86 @@ +#+TITLE: Minimal clojurescript project demoing spec + + +* Spec Intro +Spec is a library for validating data at it's core but with some more advanced features like generative testing of your functions. + +At it's core it is a global namespace on how your data is structured rules about the format of the data and how to generate the data. + +Below we simple define id as a string, =string?= is a built in function which knows how to generate a string we can then test if some data is valid using =spec/valid= as seen below :: is shorthand for current namespace wee could define id as :myns/id and is often preferred. + +#+BEGIN_SRC clojurescript :tangle ./src/test.cljs + (spec/def ::id string?) + (spec/valid? ::id "ABC-123") ;; true + (spec/valid? ::id 4) ;; false +#+END_SRC + +The errors reported by spec can be quite unwieldy to fix this you can use a library called expound to get more user friendly errors. + +* Making Specs +** Basic specs + +These are some simple spec declarations for some vary basic data types. +#+BEGIN_SRC clojurescript :tangle ./src/test.cljs + (spec/def ::id pos-int?) + (spec/def ::name string?) + (spec/def ::price decimal?) + (spec/def ::age (spec/int-in 0 100)) + (spec/def ::colours #{"red" "blue" "green"}) +#+END_SRC + +** Slightly more complex Specs +Spec allow you to apply multiple conditions that need to pass =spec/and= allow you to specify multiple conditions. +we can do regex matching with =#"regex-here"= you can also use lambda's =#(> (count %) 2)= for example to make sure the value is over 2 +#+BEGIN_SRC clojurescript :tangle ./src/test.cljs + (spec/def ::name (spec/and string? #(> (count %) 2))) + (spec/def ::name (spec/and string? #(> (count %) 2))) + (spec/def ::slug (spec/and string? #"[A-Fa-f0-9\-]+")) + (spec/def ::pos-decimal (s/and decimal? #(>= % 0))) +#+END_SRC + +** Specing out hashmaps + +Spec allow us to compose specs, this is very useful to spec out hash maps or generate api data =s/keys= allows for this we then use =:req-un= and =:opt-un= for unqualified keys words ie =:name= or =:req= and =:opt= for qualified keywords ie =:core.namespace/name= using existing specs as the keys means the values will be validated against those spec's below we are validating against spec defined further up the page. +#+BEGIN_SRC clojurescript :tangle ./src/test.cljs + (spec/def ::api-response (s/keys :req-un [::name ::slug] + :opt-un [::age])) +#+END_SRC + +** Specs with custom generators +When building more precise spec's you will hit failures to generate your data, spec by default has 100 tries, you can use :gen to tell the spec how the data will be generated, when you pull in clojure test check the library contains a load of helper generator functions like =gen/return= which can be used to write your generator functions. + +All generator functions need to return a generator =gen/return= =gen/fmap= and other can help here, in the example below we always return =123= so the value is fixed but we could compose the string or use random functions to generate any format we like. + +#+BEGIN_SRC clojurescript :tangle ./src/test.cljs +(spec/def ::demo (spec/spec string? :gen (fn [] (gen/return "123")))) +#+END_SRC + +You can do other fancy things like set frequencies that a value should appear, there is a nice guide with some examples here. +https://clojure.github.io/test.check/generator-examples.html + + +* Testing +Once you have your specs you have basically created a method to write tests, you can generate data to insert into your functions to check the results, you can also check the results against a spec. + +On top of this you can insert a spec where side effects occur, make sure the api's you interact do not change. + +** Testing reults +You can use your specs to test your results match up to your spec, you can also use specialised function specs which will generate inputs from your specs. + +#+BEGIN_SRC clojurescript :tangle ./src/test.cljs + (is (spec/valid ::name "username")) +#+END_SRC + +** Testing functions + +Using =spec/fdef= we can define the inputs and outputs from a functions and use instrument and check to test they work as expected. +#+BEGIN_SRC clojurescript :tangle ./src/test.cljs +(spec/fdef demo-function + :args (spec/cat :value int?) + :ret int?) + +(defn demo-function [value] (+ 5 value)) + +(stest/instrument `demo-function) +(stest/check `demo-function) +#+END_SRC diff --git a/spec-demo/resources/public/rename-me-index.html b/spec-demo/resources/public/rename-me-index.html new file mode 100644 index 0000000..ef36acc --- /dev/null +++ b/spec-demo/resources/public/rename-me-index.html @@ -0,0 +1,30 @@ + + + + + + + + Atom Juice Merchant Website + + + + + + + + + +
+ loading here +
+ + + + diff --git a/spec-demo/src/core.cljs b/spec-demo/src/core.cljs new file mode 100644 index 0000000..5951673 --- /dev/null +++ b/spec-demo/src/core.cljs @@ -0,0 +1,87 @@ +(ns core.demo + (:require [clojure.test.check :as tc] + [clojure.spec.alpha :as spec] + [clojure.spec.test.alpha :as stest] + [clojure.test.check.properties :as prop] + [clojure.test.check.generators :as gen])) + +;; We can define new specs with spec/def often aliased as s/def, this takes a namespace's keyword and a spec +;; there are a lot of built in specs like int? pos-int? string? etc you can also use sets +(spec/def ::id pos-int?) +(spec/def ::username string?) +(spec/def ::age (spec/int-in 0 100)) +;; example namespaced version the above is namespaced to core.demo, this is a set of allowed characters +(spec/def :core/hex-digit #{"0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "A" "B" "C" "D" "E" "F"}) + +;; Check if a value conforms to a spec +(spec/valid? ::id "ABC-123") +(spec/valid? ::id 4) +(spec/valid? ::age 99) +(spec/valid? ::age 101) + +;; we can create more custom specs like a html hex colour code, there is no built in so we +;; need a custom generator in this situation, we create a function that can generate the data + +(spec/def ::demo (spec/spec string? :gen (fn [] (gen/return "123")))) + +;; usually nicer to make a custom function, the blow fn create a vector of 6 hex characters +;; converts it into a string and append # to the beginning +(defn gen-hex-colour [] + (gen/fmap #(str "#" (apply str %)) (gen/vector (spec/gen :core/hex-digit) 6))) + +;; define the colour spec it has to pass the regex below, string? would never generate the correct Combination +;; so we supply a way of generating the data +(spec/def :core/hex-colour + (spec/spec + (spec/and string? #(re-matches #"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$" %)) + :gen gen-hex-colour)) + + +;; we can also spec out hash maps, this is very useful when dealing with json api's +;; we can fake a response data set but also validate the real response to catch changes at the edges of our code +(spec/def ::user + (spec/keys :req-un [::id ::username] + :opt-un [::age])) + + +;; We can also tests functions by defining the inputs and outputs to the functions and using gen/exercise to repeatedly send data. +;; here we are making sure an integer goes in and an integer always comes out +;; if nil or some other type is returned stest/check will fail +(spec/fdef demo-function + :args (spec/cat :value int?) + :ret int?) + +(defn demo-function [value] (+ 5 value)) + +(stest/instrument `demo-function) +(stest/check `demo-function) + +;; to generate a single value we use generate we can also use sample to get a list of values +(gen/generate (spec/gen ::id)) +(gen/generate (spec/gen ::age)) +(gen/generate (spec/gen ::user)) +(gen/sample (spec/gen ::user)) +(gen/sample (spec/gen ::id)) +(gen/sample (spec/gen ::username)) +(gen/sample (spec/gen ::age)) +(gen/sample (spec/gen :core/hex-colour)) + + +;; Bonus simple spec that you can use to insert dummy data into the demo datascript example +(spec/def :user/name string?) +(spec/def :room/name string?) +(spec/def :room/link string?) +(spec/def :room/description string?) +(spec/def :datalog/room (spec/keys + :req [:room/name] + :opt [:room/link :room/description])) +(spec/def :user/rooms (spec/coll-of :datalog/room :kind vector?)) + +(spec/def :datalog/user (spec/keys :req [:user/name] + :opt [:user/rooms])) +(gen/sample (spec/gen :datalog/user)) +(def datalog-data (gen/sample (spec/gen :datalog/user))) + +;; we can just generate a sample straight into the datalog transact function like below +;;(d/transact! conn (gen/sample (spec/gen :datalog/user))) +