Skip to content

Commit

Permalink
Merge pull request #16 from zhuangxm/inject-request-parameters
Browse files Browse the repository at this point in the history
using the idea of the web_func to inject params to function
  • Loading branch information
robertluo committed Dec 15, 2011
2 parents 5207934 + 20d5264 commit 4418c9e
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 85 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,35 @@ and if we invoke function like (+ 1 3) with token1,

then we will get a correct answer 4.

## inject invoke parameter

Sometime, some parameters that method need (like client-ip) that the
web client can not get. we can dynamic inject this kind of parameters
(from ring request) into method.

the export-commands support options like
{:params-inject [ [:remote-addr] [:header "host]]}

and clj-rpc will (get-in request [:remote-addr]) and (get-in request
[:header "host"]) as the first and second parameter of the method that
client invokes.

Example:
```clojure
(defn fn-do-someting [client-ip username]
... )

(export-commands 'mynamespace ["fn-do-something"]
{:params-inject [ [:remote-addr] ]})
```

Then the client will invoke function like (fn-do-something usernmae)
and the clj-rpc will dynamic invoke like (fn-do-something "127.0.0.1"
username)

Notice: Now, clj-rpc only supprots to inject params into the front of the
parameters that client supplied .

## user data

Sometime , we need to access data (like session) related the connection.
Expand Down
2 changes: 1 addition & 1 deletion project.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(defproject clj-rpc "0.2.1"
(defproject clj-rpc "0.2.2-SNAPSHOT"
:description "simple rpc using clojure"
:dependencies [[org.clojure/clojure "1.3.0"]
[org.clojure/tools.logging "0.2.3"]
Expand Down
122 changes: 89 additions & 33 deletions src/clj_rpc/context.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
(:require [ring.middleware.cookies :as cookies]
[clj-rpc.user-data :as data]))

(defn wrap-client-ip
"let :remote-addr represents the real client ip even though
the client connects through a proxy server"
[handler]
(fn [request]
(let [ip-from-proxy (get-in request [:headers "X-Forwarded-For"])
request (if ip-from-proxy
(assoc request :remote-addr ip-from-proxy)
request)]
(handler request))))

(defn wrap-context
"accoding to the token
(come from cookie in the http request or the token parameters)
Expand All @@ -22,52 +33,97 @@

(defn add-context
"add context to specific command
options is a map include several keys
options is a collection include several keys
like [ [:requre-context true] [:params-checks ...] [... ...] ]
the meaning of option:
:require-context (true or false default false)
whether this command must have context
:params-checks
check parameters of the method be invoked statisfy the sepcific requirements
example :
{0 [:username]} -->
the first parameter must equals (get-in context [:username])"
the first parameter must equals (get-in context [:username])
:params-inject
inject parameters from request into the function
example :
[ [:remote-addr] [:server-name] ] -->
client-ip and server-name as the first and second parameter of the function
if client invoke (fun param1 param2) then the acutally invoke will be like
(fun client-ip server-name param1 param2)"
[command options]
(merge command options))
(assoc command :options options))

(defn add-context-to-command-map
"add context options to all the command in the command-map
options : same as add-context"
[command-map options]
(into {} (map (fn [[k v]] [k (merge v options)]) command-map)))
(into {} (map (fn [[k v]] [k (add-context v options)]) command-map)))

(defn error-method-request?
"return true if method-reqeust error
else false"
[method-request]
(boolean (:error method-request)))

(defmulti render-method-request
"adjust method request by option
return new method-request"
(fn [option-key option-value request method-request]
option-key))

(defn- check-authorization
"return nil if the command don't need authrization or
the context include the authorization information
else return {:code :unauthorized}"
[context command]
(when (and (get command :require-context)
(not (seq context)))
{:code :unauthorized}))
;;render method-request :require-context option
;;return method-request with error or original method-request
(defmethod render-method-request :require-context
[_ option-value request method-request]
(if (and option-value
(not (seq (get request :context) )))
(assoc method-request :error {:code :unauthorized})
method-request))

(defn- check-params
"return nil
if the params of the command satisfy the requirement of the command
provided specific context
else return {:code invalid-params :message error-message}"
[context command command-params]
(letfn [(fn-check [[k v]]
(if (not= (nth command-params k) (get-in context v))
;;render method-request :params-check option
;;return method-request with params error or original method-request
(defmethod render-method-request :params-check
[_ option-value request method-request]
(let [{context :context} request
{params :params} method-request
fn-check
(fn [[k v]]
(if (not= (nth params k) (get-in context v))
{:code :invalid-params
:message (str "the " k "th param must be " (get-in context v ))} ))]
(->> (:params-checks command)
(map fn-check )
(filter identity)
first)))
:message (str "the " k "th param must be "
(get-in context v))}))]
(if-let [error (->> option-value
(map fn-check )
(filter identity)
first)]
(assoc method-request :error error)
method-request)))

;;render method-request :params-inject option
;;inject the params from request needed by the command
;;into the actual params
;;return the new method-request
(defmethod render-method-request :params-inject
[_ option-value request method-request]
(if option-value
(update-in method-request [:params]
#(concat (map (partial get-in request) option-value) %))
method-request))

;;default throw RuntimeException
(defmethod render-method-request :default
[option-key option-value request method-request]
(throw (RuntimeException. (str "Unknown command option: " option-key
"method-request: " method-request ) )) )

(defn check-context
"check whether the command and params statisfy the requirement
if success return nil
else return {:code error-code :message error-message}
error-code refer to clj-rpc.rpc.clj"
[command context params]
(or (check-authorization context command)
(check-params context command params)))
(defn adjust-method-request
"return new method-request (possible with error message)"
[cmd request method-request]
(loop [options (:options cmd)
m-r method-request]
(let [[option-key option-value] (first options)]
(if (or (nil? option-key) (error-method-request? m-r))
m-r
(recur (rest options)
(render-method-request option-key option-value request m-r))))))
10 changes: 6 additions & 4 deletions src/clj_rpc/rpc.clj
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@
"execute a function f with params
return rpc response
id : used for generate the response"
[f params id]
[f {:keys [params id error]}]
(try
(if f
(mk-response (apply f params) id)
(mk-error :method-not-found id))
(if error
(mk-error (:code error) id (:message error))
(if f
(mk-response (apply f params) id)
(mk-error :method-not-found id)))
(catch ArityException e (mk-error :invalid-params id (.getMessage e)) )
(catch Exception e (mk-error :internal-error id (.getMessage e)))))
42 changes: 22 additions & 20 deletions src/clj_rpc/server.clj
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@
(defn export-commands
"export all functions (fn-names is null or empty)
or specify functions in the namespace ns
invoker of this method must notice the order of the options
options: options is a collection include several keys
like [ [:requre-context true] [:params-checks ...] [... ...] ]
options: is a map include several keys
the meaning of option:
:require-context (true or false default false)
whether this command must have context
:params-checks
Expand All @@ -52,7 +55,7 @@
(export-commands \"clojure.core\" nil)
(export-commands 'clojure.core ['+])
(export-commands 'clojure.core [\"+\"]
{:require-context true :params-check {0 [:id]}})"
[ [:require-context true] [:params-check {0 [:id]}] ])"
[ns fn-names & [options]]
(let [ns (symbol ns)]
(require ns)
Expand All @@ -66,15 +69,13 @@
"get the function from the command-map according the method-name and
execute this function with args
return the execute result"
[command-map context {:keys [method params id]}]
(logging/debug "execute-command == method-name: "
method " params: " params " id: " id)
(let [cmd (command-map method)
[command-map request method-request]
(logging/debug "execute-command == " method-request)
(let [cmd (command-map (:method method-request))
f (and cmd (command/.func cmd))
check-result (context/check-context cmd context params)]
(if check-result
(rpc/mk-error (:code check-result) id (:message check-result))
(rpc/execute-method f params id))))
new-method-request (context/adjust-method-request
cmd request method-request)]
(rpc/execute-method f new-method-request)))

(defn help-commands
"return the command list"
Expand All @@ -90,9 +91,9 @@
(defn rpc-invoke
"invoke rpc method
rpc-request can a map (one invoke) or a collection of map (multi invokes)"
[command-map context rpc-request]
[command-map request rpc-request]
(letfn [(fn-execute [r]
(execute-command command-map context
(execute-command command-map request
(change-str->keyword r)))]
(if (map? rpc-request)
(fn-execute rpc-request)
Expand All @@ -102,11 +103,11 @@
(ANY "/:s-method/help" [s-method]
(when-let [[f-encode] (protocol/serialization s-method)]
(f-encode (help-commands @commands))))
(POST "/:s-method/invoke" [s-method :as {context :context body :body}]
(let [rpc-request (slurp body)]
(POST "/:s-method/invoke" [s-method :as reqeust]
(let [rpc-request (slurp (:body reqeust))]
(logging/debug "invoking (" s-method ") request: " rpc-request)
(let [[f-encode f-decode] (protocol/serialization s-method)]
(f-encode (rpc-invoke @commands context (f-decode rpc-request))))))
(f-encode (rpc-invoke @commands reqeust (f-decode rpc-request))))))
(route/not-found "invalid url"))

;;define a jetty-instance used to start or stop
Expand All @@ -120,11 +121,12 @@
(reset! jetty-instance nil))

(defn build-hander [options]
(handler/site
(context/wrap-context main-routes
(:fn-get-context options)
(:cookie-attrs options)
(:token-cookie-key options))))
(-> main-routes
(context/wrap-context (:fn-get-context options)
(:cookie-attrs options)
(:token-cookie-key options))
(context/wrap-client-ip)
handler/site))

(defn start
"start jetty server
Expand Down
9 changes: 6 additions & 3 deletions src/clj_rpc/user_data.clj
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,19 @@
"存储用户数据,如果原来没有token,那么创建一个新的token
返回 data"
[data]
(do (if (not @*atom-token*) (reset! *atom-token* (uuid)) )
(swap! atom-user-datas assoc @*atom-token* data )
data))
(do
;;treat @atom-token* "" as nil
(if (not (seq @*atom-token*)) (reset! *atom-token* (uuid)) )
(swap! atom-user-datas assoc @*atom-token* data )
data))

(defn delete-user-data!
"删除用户相关数据,
纯粹副作用,返回nil"
[]
(do
(swap! atom-user-datas dissoc @*atom-token*)
(reset! *atom-token* "")
nil))

;;TODO 增加用户数据定期清理
54 changes: 33 additions & 21 deletions test/clj_rpc/test/context.clj
Original file line number Diff line number Diff line change
@@ -1,33 +1,45 @@
(ns clj-rpc.test.context
(:use [clj-rpc.context :reload true]
[clojure.test])
[clojure.test]
[midje.sweet])
(:require [clj-rpc.command :as command]))


(deftest check-add-context-to-command-map
(let [command-map {"str" (command/mk-command "str" #'str)
"concat" (command/mk-command "concat" #'concat)}
options {:require-context true :params-check {0 1}}
options [ [:require-context true] [:params-check {0 1}]]
new-map (add-context-to-command-map command-map options)
vs (vals new-map)]
(is (= (get-in (first vs) [:params-check]) {0 1}))
(is (= (get-in (second vs) [:params-check]) {0 1}))))
(is (= (get (first vs) :options) options))
(is (= (get (second vs) :options) options))))

(deftest test-check-context
(facts
"check whether or not the command and params statifies context requirement"
(let [cmd (-> (command/mk-command "concat" #'concat)
(add-context {:require-context true
:params-checks {0 [:p1] 1 [:p2]}}))
cmd-not-require (command/mk-command "str" #'str)]
(is (nil? (check-context cmd {:p1 [1 2] :p2 [3 4]}
[ [1 2] [3 4] [5 6]]))
"stastify authorized and params requirement")
(is (= :unauthorized (:code (check-context cmd nil [ [1 2] [3 4] [5 6]])))
"do not statify authorization requirement")
(is (= :unauthorized (:code (check-context cmd {} [ [1 2] [3 4] [5 6]])))
"do not statify authorization requirement")
(is (= :invalid-params (:code (check-context cmd {:p1 [1 2]}
[ [1 2] [3 4] [5 6]])))
"do not statify params check requirement")
(is (nil? (check-context cmd-not-require nil ["str1" "str2"]))
"do not require context check")))
(add-context [ [:require-context true]
[:params-check {0 [:p1] 1 [:p2]}]]))
cmd-not-require (command/mk-command "str" #'str)
method-request {:method "method" :params [ [1 2] [3 4] [5 6]]}]
;;stastify authorized and params requirement
(adjust-method-request cmd {:context {:p1 [1 2] :p2 [3 4]}}
method-request) => method-request

;;do not statify authorization requirement
(adjust-method-request cmd nil method-request) => (contains {:error {:code :unauthorized}})
(adjust-method-request cmd {} method-request) => (contains {:error {:code :unauthorized}})

;;do not statify params check requirement
(get-in (adjust-method-request cmd {:context {:p1 [1 2] }} method-request)
[:error :code]) => :invalid-params

;;do not require context check
(adjust-method-request cmd-not-require nil {:method "method" :params ["str1" "str2"]})
=> {:method "method" :params ["str1" "str2"]}))

;.;. The work itself praises the master. -- CPE Bach
(facts "test inject parasm"
(let [cmd (-> (command/mk-command "str" #'str)
(add-context {:params-inject [ [:remote-addr] [:inject]]})) ]
(adjust-method-request cmd {:remote-addr "192.168.1.1" :inject "inject-param2"}
{:method "method" :params ["p1" "p2"]})
=> {:method "method" :params ["192.168.1.1" "inject-param2" "p1" "p2"]}))
Loading

0 comments on commit 4418c9e

Please sign in to comment.