I am trying to do this in webnoir.
This works:
(defpage [:post "/testurl] {:keys [name phone]}
(html5
(str "name: " name)
(str "phone: " phone)))
Now I want to generate defpages for many modules, each has a list of different fields. And I want to call the defpages from a function. The defpage must accept post for the fields.
Basically I have this: (def fields1 ["Name" "Phone" "Email" "xyz"])
And I would like to pass this to defpage, instead of having to specify the keys manually.
The fields might change in the future and that's why I want my code to pick up the fields and create the defpages dynamically on server startup.
Is it possible?
Thank you for all your help!
You can do this with a macro:
(defmacro defpages [pages]
`(do
~#(map (fn [page]
`(~'defpage [:post ~(str "/" (page :name))]
{:keys ~(into [] (map symbol (page :fields)))}
(~'html5
~#(map (fn [field]
`(str ~(str field ": ")
~(symbol field)))
(page :fields))))) pages)))
(defpages [{:name "testurl"
:fields ["name" "phone"]}
{:name "user"
:fields ["age" "address"]}])
Related
I have written this function to convert a vector of maps into string. There is a second map called field-name-to-columns which contains a mapping between the field-name and the actual name of columns in my database.
My goal is to get a string like in the example where if the key is not present in the field-name-to-columns be ignored. Plus I want to have “client.name DESC” as a default if the :sorting key is empty or missing or none of the field-names matches any key in field-name-to-columns.
(def field-name-to-columns {"name" "client.name"
"birthday" "client.birthday"
"last-name" "client.last-name"
"city" "client.city"})
(def request {:sorting [{:field-name "city" :desc true}
{:field-name "country" :desc true}
{:field-name "birthday" :desc false}]})
(defn request-to-string
"this function creates the sorting part of query"
[sorting]
(if (empty? sorting)
(str "client.name" "DESC")
(->> (filter some? (for [{:keys [field-name desc]} sorting]
(when (some? (field-name-to-columns field-name)) (str (field-name-to-columns field-name) (when desc " DESC")))))
(st/join ", "))))
(request-to-string (request :sorting))
=>"client.city DESC, client.birthday"
Any comments on how to write this function more readable would be highly appriciated
What you've written is very reasonable in my opinion. I'd just add some whitespace for a visual break and tidy up your null handling a bit: it's silly to put nulls into the result sequence and then filter them out, rather than producing only non-nil values.
(defn request-to-string [sorting]
(str/join ", "
(or (seq (for [{:keys [field-name desc]} sorting
:let [column (field-name-to-columns field-name)]
:when column]
(str column (when desc " DESC"))))
["client.name DESC"])))
I've also moved the str/join up front; this is a stylistic choice most people disagree with me about, but you asked for opinions. I just think it's nice to emphasize that part by putting it up front, since it's an important part of the process, rather than hiding it at the back and making a reader remember the ->> as they read through the body of the function.
I also prefer using or rather than if to choose defaults, but it's not especially beautiful here. I also considered (or (non-empty (join ...)) "client.name DESC"). You might prefer either of these options, or your own choice, but I thought you'd like to see alternate approaches.
Here is one idea, based on my favorite template project.
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test)
(:require
[tupelo.string :as str]))
(def field-name->columns {"name" "client.name"
"birthday" "client.birthday"
"last-name" "client.last-name"
"city" "client.city"})
(defn field->string
[{:keys [field-name desc]}]
; give names to intermediate and/or temp values
(let [col-name (field-name->columns field-name)]
(when (some? col-name)
(str col-name
(when desc " DESC")))))
(defn request->string
"this function creates the sorting part of query"
[sorting]
; accept only valid input
(when-not sorting ; WAS: (str "client.name" "DESC")
(throw (ex-info "sorting array required, value=" {:sorting sorting})))
; give names to intermediate values
(let [fiels-strs (filter some?
(for [entry sorting]
(field->string entry)))
result (str/join ", " fiels-strs)]
result))
and unit tests
(verify
(is= (field->string {:field-name "city", :desc true}) "client.city DESC")
(is= (field->string {:field-name "country", :desc true}) nil)
(is= (field->string {:field-name "birthday", :desc false}) "client.birthday")
(let [sorting [{:field-name "city" :desc true}
{:field-name "country" :desc true}
{:field-name "birthday" :desc false}]]
(is= (spyx-pretty (request->string sorting))
"client.city DESC, client.birthday")))
I prefer the (->> (map ...) (filter ...)) pattern over the for macro:
(defn request-to-string [sorting]
(or (->> sorting
(map (fn [{:keys [field-name desc]}]
[(field-name-to-columns field-name)
(when desc " DESC")]))
(filter first)
(map #(apply str %))
(clojure.string/join ", ")
not-empty)
"client.name DESC"))
My Clojure app needs some handlers to do business, those handlers will preform some common parameters check, so I use a macro to do this like below:
(defmacro defapihandler [handler-name params & body]
`(defn ~handler-name ~params
(let [keyed-params# (map keyword '~params)
checked-ret# (check-param (zipmap keyed-params# ~params))]
(if (:is-ok checked-ret#)
(do ~#body)
(-> (response {:code 10000
:msg (format " %s are missing !!!" (:missed-params checked-ret#))})
(status 400))))))
Then I can use above macro like this:
(defapihandler create-user [username password birthday]
;; todo
)
Everything is fine this way.
As you can see, the params of generated fn is constructed directly from args of the marco, exception raised when params of generated fn can't constructed directly.
Take a example:
The params of the macro defapihandler now became like this:
[{:key :username :checker [not-nil?]} {:key :password :checkers [is-secure?]} ...]
In the macro, I want to build the param of the generated fn dynamicly like this:
(defmacro defapihandler [handler-name params & body]
`(defn ~handler-name [passed-param#]
(let [param-keys# (vec (map (comp symbol name :key)
~params))
{:keys param-keys#} passed-param#]
;; some check
(do ~#body))))
(defapihandler create-user [{:key :username :checkers []}]
(println username))
The structure of passed-param looks like this: {:username "foo" :password "bar"}
Now I want to construct the variables used in body block in let block, Then following exception is thrown:
Caused by java.lang.IllegalArgumentException
Don't know how to create ISeq from: clojure.lang.Symbol
macroexpand create-user got this:
(defn create-user [passed-param__10243__auto__]
(let [param-keys__10244__auto__ (vec
(map
(comp symbol name :key)
[{:key :username,
:checkers []}]))
{:keys param-keys__10244__auto__} passed-param__10243__auto__]
(do (println username))))
I suspect this exception is related to dynamic var used in let destructuring form, if my suspect is right, then how to construct variables used in body block ?
You need to pull the clause that builds your params-key vector out of the generated code.
So:
(defmacro defapihandler [handler-name params & body]
(let [param-keys (map (comp symbol name :key) params)]
`(defn ~handler-name [passed-param#]
(let [{:keys [~#param-keys]} passed-param#]
;; some check
(do ~#body)))))
Or if you don't need passed-param#:
(defmacro defapihandler [handler-name params & body]
(let [param-keys (map (comp symbol name :key) params)]
`(defn ~handler-name [{:keys [~#param-keys]}]
;; some check
(do ~#body))))
The below code I have found from a book (Functional Programming Patterns in Scala and Clojure). The for statement uses close-zip? to filter out people outside of the zips and then it generates a greeting to the people who are left. However, I am not quite sure how people should look like as argument for generate-greetings and print-greetings functions?
(def close-zip? #{19123 19103})
(defn generate-greetings [people]
(for [{:keys [name address]} people :when (close-zip? (address :zip-code))]
(str "Hello, " name ", and welcome to the Lambda Bar And Grille!")))
(defn print-greetings [people]
(doseq [{:keys [name address]} people :when (close-zip? (address :zip-code))]
(println (str "Hello, " name ", and welcome to the Lambda Bar And Grille!"))))
They need to be maps with :name and :address keys, like:
{:name "A Person", :address {:zip-code 19103}}
for will take each element from people and assign each one to {:keys [name address]}. This is called destructuring, and it's just a convenience. It's the same as saying:
(for [person people
:let [name (:name person)
address (:address person)]
:when (close-zip? (:zip-code address))]
(str ...))
I have created small compojure web application, which can display multiple values, fetched from other website, using provided URL. At the moment, this URL is hard coded in one of my functions, and now I would like to add feature for dynamical URL creation, based upon values in text field and checkbox.
This is how my page looks like:
(defn view-layout [& content]
(html [:body content]))
(defn view-input []
(view-layout
[:h2 "Find"]
[:form {:method "post" :action "/"}
( for [category ["Cat1" "Cat2" "Cat3"]]
[:input {:type "checkbox" :id category } category ] )
[:br]
[:input {:type "text" :id "a" :value "insert manga name"}] [:br]
[:input.action {:type "submit" :value "Find"}]
[:a {:href "/downloads"} "Downloads"]]))
(defn view-output []
(view-layout
[:h2 "default images"]
[:form {:method "post" :action "/"}
(for [name (get-content-from-url (create-url))]
[:label name [:br]]
)]))
(defn create-manga-url
[]
"http://www.mysite.net/search/?tfield=&check=000")
Here are the routes:
(defroutes main-routes
(GET "/" []
(view-input))
(GET "/downloads" []
(view-downloads))
(POST "/" []
(view-output) ))
At the moment, I need help with (create-url) function (returns a string), where I would like to fetch all fields, mandatory for my search (one text field and 3 checkboxe's) , and parse values from them, which will be fed into (concatenated) the URL - for checkbox, if checked, the check section will have value 1, instead of 0, or remain 0 if not (check=100, or 010, 011 if two check boxes were selected) . In case of text field, the tfield=userinputtext.
EDIT
I spent a lot of time as a .Net and Java developer, and this part of compojure is a total mystery for me.
This is what I would like to achieve with (create-url) function (pseudo code written in OO style):
(defn create-url [*text_field cbox1 cbox2 cbox3*]
(def url "http://www.mysite.net/search/?")
(def tfield "tfield=")
(def cbox "&check=")
(if (checked? cbox1)
(str cbox "1")
(str cbox "0"))
(if (checked? cbox2)
(str cbox "1")
(str cbox "0"))
(if (checked? cbox3)
(str cbox "1")
(str cbox "0"))
(str tfield (:value text_field))
(str url tbox cbox))
I apologize for how this pseudo code looks like, but this is the part that I would like to learn: How can I scoop data from form, and parse it (in this case i would like to attachh values from form fields into the string)
Can anyone help me with this?
First, you need to add 'name' attributes to your HTML input elements. The 'id' attributes aren't sent to the server on post.
Next, I guess a quick way of doing this similar to your example is:
(POST "/" [a Cat1 Cat2 Cat3] (create-url a [Cat1 Cat2 Cat3]))
and then something like this:
(defn checked? [c]
(and c (= c "on")))
(defn checked->num [c]
(if (checked? c) "1" "0"))
(defn create-url [a cats]
(str "x?tfield=" a "&check="
(apply str (for [c cats] (checked->num c)))))
Or just drop the two helpers:
(defn create-url [a cats]
(str "x?tfield=" a "&check="
(apply str (map #(if (= "on" %) "1" "0") cats))))
As total clojure noob, I am trying to start one small tutorial app, in order to get familiar with compojure. It's a small application which lets user add two numbers, and after clicking on button displays their sum on the other page. I followed instruction from Mark McGranaghan blog. Everything seems ok, until I try to get sum of two numbers I have entered, instead of getting result, I am redirected to the same page (so basically I am stuck on first step of this tutorial). After checking the code, it seems that NumberFormatException is triggered when input parsing takes place (for some reason). In all my tests, I have tried to input all kinds of number format , but with no success. Here is the simplest code version , for which author said should work (I have tried the latest version from github site- same scenario: NFE):
(ns adder.core
(:use compojure.core)
(:use hiccup.core)
(:use hiccup.page-helpers))
(defn view-layout [& content]
(html
(doctype :xhtml-strict)
(xhtml-tag "en"
[:head
[:meta {:http-equiv "Content-type"
:content "text/html; charset=utf-8"}]
[:title "adder"]]
[:body content])))
(defn view-input []
(view-layout
[:h2 "add two numbers"]
[:form {:method "post" :action "/"}
[:input.math {:type "text" :name "a"}] [:span.math " + "]
[:input.math {:type "text" :name "b"}] [:br]
[:input.action {:type "submit" :value "add"}]]))
(defn view-output [a b sum]
(view-layout
[:h2 "two numbers added"]
[:p.math a " + " b " = " sum]
[:a.action {:href "/"} "add more numbers"]))
(defn parse-input [a b] ;; this is the place where problem occures
[(Integer/parseInt a) (Integer/parseInt b)])
(defroutes app
(GET "/" []
(view-input))
(POST "/" [a b]
(let [[a b] (parse-input a b)
sum (+ a b)]
(view-output a b sum)))
Can anyone tell me better way to pars the input values, in order to avoid this exception?I have tried couple of techniques , but nothing worked for me. I am using Leningen v1.7.1 with clojure 1.3 on win 7 machine.
Here is content of my project.clj file:
(defproject adder "0.0.1"
:description "Add two numbers."
:dependencies
[[org.clojure/clojure "1.3.0"]
[org.clojure/clojure-contrib "1.1.0"]
[ring/ring-core "1.0.2"]
[ring/ring-devel "1.0.2"]
[ring/ring-jetty-adapter "1.0.2"]
[compojure "1.0.1"]
[hiccup "0.3.8"]]
:dev-dependencies
[[lein-run "1.0.0"]])
and run.clj script:
(use 'ring.adapter.jetty)
(require 'adder.core)
(let [port (Integer/parseInt (get (System/getenv) "PORT" "8080"))]
(run-jetty #'adder.core/app {:port port}))
Thanks.
You are using compojure 1.0.1, the example in the blog you are following is using compojure 0.4.0.
As of version 0.6.0, Compojure no longer adds default middleware to routes. This means you must explicitly add the wrap-params and wrap-cookies middleware to your routes.
Source: https://github.com/weavejester/compojure
So you need to explicitly add the wrap-params middleware. So the following changes are required...
(ns adder.core
(:use ; change to idiomatic usage of :use
[compojure.core]
[hiccup.core]
[hiccup.page-helpers]
[ring.middleware.params :only [wrap-params]])) ; add middleware for params
(defn view-layout [& content]
(html
(doctype :xhtml-strict)
(xhtml-tag "en"
[:head
[:meta {:http-equiv "Content-type"
:content "text/html; charset=utf-8"}]
[:title "adder"]]
[:body content])))
(defn view-input []
(view-layout
[:h2 "add two numbers"]
[:form {:method "post" :action "/"}
[:input.math {:type "text" :name "a" :id "a"}] [:span.math " + "]
[:input.math {:type "text" :name "b" :id "a"}] [:br]
[:input.action {:type "submit" :value "add"}]]))
(defn view-output [a b sum]
(view-layout
[:h2 "two numbers added"]
[:p.math a " + " b " = " sum]
[:a.action {:href "/"} "add more numbers"]))
(defn parse-input [a b]
[(Integer/parseInt a) (Integer/parseInt b)])
(defroutes main-routes ; needs to be renamed
(GET "/" []
(view-input))
(POST "/" [a b]
(let [[a b] (parse-input a b)
sum (+ a b)]
(view-output a b sum))))
(def app (wrap-params main-routes)) ; wrap the params to allow destructuring to work