initial import
This commit is contained in:
commit
31a1807ec7
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
/target
|
||||
/lib
|
||||
/classes
|
||||
/checkouts
|
||||
pom.xml
|
||||
*.jar
|
||||
*.class
|
||||
.lein-deps-sum
|
||||
.lein-failures
|
||||
.lein-plugins
|
68
README.rest
Normal file
68
README.rest
Normal file
|
@ -0,0 +1,68 @@
|
|||
clj-rhino
|
||||
=========
|
||||
|
||||
a nice wrapper to handle rhino from clojure
|
||||
|
||||
who?
|
||||
----
|
||||
|
||||
marianoguerra
|
||||
|
||||
why?
|
||||
----
|
||||
|
||||
the java api for rhino is not really nice
|
||||
|
||||
how?
|
||||
----
|
||||
|
||||
you can see the tests for some usage, here are some REPL examples:
|
||||
|
||||
user=> (require '[clj-rhino :as js])
|
||||
nil
|
||||
user=> (def sc (js/new-safe-scope))
|
||||
#'user/sc
|
||||
user=> (js/eval sc "1 + 1")
|
||||
2
|
||||
user=> (js/eval sc "a = 1 + 1")
|
||||
2
|
||||
user=> (js/get sc "a")
|
||||
2
|
||||
user=> (js/get sc "b")
|
||||
#<UniqueTag org.mozilla.javascript.UniqueTag@172897f: NOT_FOUND>
|
||||
user=> (js/undefined? (js/get sc "b"))
|
||||
true
|
||||
user=> (js/get sc "b" :w00t?)
|
||||
:w00t?
|
||||
user=> (js/defined? (js/get sc "b"))
|
||||
false
|
||||
user=> (js/set! sc "b" 42)
|
||||
nil
|
||||
user=> (js/defined? (js/get sc "b"))
|
||||
true
|
||||
user=> (js/get sc "b" :w00t?)
|
||||
42
|
||||
user=> (js/eval sc "a = {name: 'spongebob'}")
|
||||
#<NativeObject [object Object]>
|
||||
user=> (js/get-in sc [:a :name])
|
||||
"spongebob"
|
||||
user=> (js/get-in sc [:a :age])
|
||||
#<UniqueTag org.mozilla.javascript.UniqueTag@172897f: NOT_FOUND>
|
||||
user=> (js/get-in sc [:a :age] :dont-know)
|
||||
:dont-know
|
||||
user=> (def compiled-fun (js/compile-function sc "function (a, b) { return a + b; }" :filename "foo.js"))
|
||||
#'user/compiled-fun
|
||||
user=> (js/set! sc "add" compiled-fun)
|
||||
nil
|
||||
user=> (js/eval sc "add(1, 3)")
|
||||
4.0
|
||||
user=>
|
||||
|
||||
license?
|
||||
--------
|
||||
|
||||
it seems the clojure people under this circumstances say something like:
|
||||
|
||||
Copyright © 2013 marianoguerra
|
||||
|
||||
Distributed under the Eclipse Public License, the same as Clojure.
|
7
project.clj
Normal file
7
project.clj
Normal file
|
@ -0,0 +1,7 @@
|
|||
(defproject clj-rhino "0.1.0-SNAPSHOT"
|
||||
:description "library to ease the interaction between rhino and clojure"
|
||||
:url "http://github.com/marianoguerra/clj-rhino"
|
||||
:license {:name "Eclipse Public License"
|
||||
:url "http://www.eclipse.org/legal/epl-v10.html"}
|
||||
:dependencies [[org.clojure/clojure "1.4.0"]
|
||||
[org.mozilla/rhino "1.7R4"]])
|
117
src/clj_rhino.clj
Normal file
117
src/clj_rhino.clj
Normal file
|
@ -0,0 +1,117 @@
|
|||
(ns clj-rhino
|
||||
(:refer-clojure :exclude (eval get get-in set!))
|
||||
(:import [org.mozilla.javascript Context UniqueTag]))
|
||||
|
||||
(def insecure-vars ["isXMLName" "uneval" "InternalError" "JavaException"
|
||||
"With" "Call" "Script" "Iterator" "StopIteration",
|
||||
"Packages" "java" "javax" "org" "com" "edu" "net"
|
||||
"getClass" "JavaAdapter" "JavaImporter" "Continuation"
|
||||
"XML" "XMLList" "Namespace" "QName"])
|
||||
|
||||
(defn with-context [fun]
|
||||
"create a context call fun with it and safelly exit the context"
|
||||
(let [ctx (Context/enter)]
|
||||
(try
|
||||
(fun ctx)
|
||||
(finally (Context/exit)))))
|
||||
|
||||
(defn with-context-if-nil [ctx fun]
|
||||
"create a context if ctx is nil, otherwise use ctx and call fun with it,
|
||||
exit safelly after if ctx was created here, otherwise is up to the caller
|
||||
(which should be inside a with-context somewhere up the call stack)"
|
||||
(if ctx
|
||||
(fun ctx)
|
||||
(with-context fun)))
|
||||
|
||||
(defn eval [scope code & {:keys [ctx filename line-number sec-domain]}]
|
||||
(with-context-if-nil ctx (fn [ctx1]
|
||||
(.evaluateString ctx1 scope code
|
||||
(or filename "<eval>")
|
||||
(or line-number 1) sec-domain))))
|
||||
|
||||
(defn undefined? [value]
|
||||
"return true if value is undefined"
|
||||
(= value (. UniqueTag NOT_FOUND)))
|
||||
|
||||
(def defined? (comp not undefined?))
|
||||
|
||||
(defn set! [scope name value]
|
||||
"bind an object to a name in scope"
|
||||
(.put scope name scope value))
|
||||
|
||||
(defn get
|
||||
"return the object referenced by var-name in scope,
|
||||
UniqueTag.NOT_FOUND if not found or not-found if supplied"
|
||||
([scope var-name]
|
||||
; TODO: return undefined when scope doesn't have .get
|
||||
(.get scope (name var-name) scope))
|
||||
([scope var-name not-found]
|
||||
(let [result (.get scope (name var-name) scope)]
|
||||
(if (undefined? result)
|
||||
not-found
|
||||
result))))
|
||||
|
||||
(defn get-in
|
||||
"Returns the value in a nested scope,
|
||||
where ks is a sequence of keys. Returns nil if the key is not present,
|
||||
or the not-found value if supplied."
|
||||
([scope ks]
|
||||
(reduce get scope ks))
|
||||
([scope ks not-found]
|
||||
(loop [sentinel (Object.)
|
||||
scope scope
|
||||
ks (seq ks)]
|
||||
(if ks
|
||||
(let [scope (get scope (first ks) sentinel)]
|
||||
(if (identical? sentinel scope)
|
||||
not-found
|
||||
(recur sentinel scope (next ks))))
|
||||
scope))))
|
||||
|
||||
|
||||
(defn new-root-scope [& [ctx sealed vars-to-remove]]
|
||||
"create a new root js scope and return it
|
||||
make it sealed if sealed is true
|
||||
remove vars-to-remove if non nil (a seq of strings)"
|
||||
(with-context-if-nil ctx (fn [ctx]
|
||||
(let [scope (.initStandardObjects ctx nil true)]
|
||||
; force loading RegExp
|
||||
(eval scope "RegExp;" :ctx ctx)
|
||||
|
||||
(dorun (map #(.delete scope %) (or vars-to-remove [])))
|
||||
|
||||
(when sealed
|
||||
(.sealObject scope))
|
||||
|
||||
scope))))
|
||||
|
||||
(defn new-safe-root-scope [& [ctx]]
|
||||
"create a new root js scope removing dangerous references and sealing it"
|
||||
(new-root-scope ctx true insecure-vars))
|
||||
|
||||
(defn new-scope [& [ctx parent-scope vars-to-remove]]
|
||||
"create a new scope with parent-scope as parent, if parent-scope is nil
|
||||
create it"
|
||||
(with-context-if-nil ctx (fn [ctx]
|
||||
(let [parent-scope (or parent-scope
|
||||
(new-root-scope ctx true
|
||||
vars-to-remove))
|
||||
scope (.newObject ctx parent-scope)]
|
||||
|
||||
(doto scope
|
||||
(.setPrototype parent-scope)
|
||||
(.setParentScope nil))
|
||||
|
||||
scope))))
|
||||
|
||||
|
||||
(defn new-safe-scope [& [ctx]]
|
||||
"create a new scope using a safe root scope as parent"
|
||||
(new-scope ctx (new-safe-root-scope ctx)))
|
||||
|
||||
(defn compile-function [scope code & {:keys [ctx filename line-number sec-domain]}]
|
||||
"compile and return function defined in code"
|
||||
(with-context-if-nil ctx (fn [ctx]
|
||||
(.compileFunction ctx scope code
|
||||
(or filename "<eval>")
|
||||
(or line-number 1) sec-domain))))
|
67
test/clj_rhino/core_test.clj
Normal file
67
test/clj_rhino/core_test.clj
Normal file
|
@ -0,0 +1,67 @@
|
|||
(ns clj-rhino.core-test
|
||||
(:import [org.mozilla.javascript Context UniqueTag EvaluatorException])
|
||||
(:use clojure.test)
|
||||
(:require [clj-rhino :as js]))
|
||||
|
||||
(defn- assert-var-undefined [scope]
|
||||
(fn [name]
|
||||
(js/eval scope (str "a = typeof " name))
|
||||
(is (= (js/get scope "a") "undefined"))))
|
||||
|
||||
(deftest js-test
|
||||
(testing "undefined? works"
|
||||
(is (not (js/undefined? 1)))
|
||||
(is (js/undefined? (. UniqueTag NOT_FOUND))))
|
||||
|
||||
(testing "get returns undefined if get'ing inexistent var"
|
||||
(let [scope (js/new-scope)]
|
||||
(is (js/undefined? (js/get scope "foo")))))
|
||||
|
||||
(testing "get returns default if get'ing inexistent var and default provided"
|
||||
(let [scope (js/new-scope)]
|
||||
(is (= (js/get scope "foo" 4) 4))))
|
||||
|
||||
(testing "safe scope doesn't have dangerous references"
|
||||
(let [scope (js/new-safe-scope)
|
||||
unsafe-scope (js/new-scope)]
|
||||
|
||||
(js/eval scope "a = typeof With")
|
||||
(js/eval unsafe-scope "a = typeof With")
|
||||
(is (= (js/get scope "a") "undefined"))
|
||||
(is (= (js/get unsafe-scope "a") "function"))
|
||||
|
||||
(dorun (map (assert-var-undefined scope) js/insecure-vars))))
|
||||
|
||||
(testing "set function sets a variable in the scope"
|
||||
(let [scope (js/new-safe-scope)]
|
||||
(js/set! scope "a" 2)
|
||||
(is (= (js/get scope "a") 2))))
|
||||
|
||||
(testing "functions can be compiled"
|
||||
(let [scope (js/new-safe-scope)
|
||||
code "function (a, b) { return a + b; }"
|
||||
compiled-fun (js/compile-function scope code :filename "foo.js")]
|
||||
(js/set! scope "add" compiled-fun)
|
||||
(is (= (js/eval scope "add(1, 2)") 3.0))))
|
||||
|
||||
(testing "root scope doesn't allow var modifications"
|
||||
(let [scope (js/new-safe-root-scope)]
|
||||
(is (thrown? EvaluatorException (js/eval scope "Object = null")))))
|
||||
|
||||
(testing "code can be evaled"
|
||||
(let [scope (js/new-scope)]
|
||||
(js/eval scope "a = 1;")
|
||||
(is (= (js/get scope "a") 1.0))
|
||||
|
||||
(js/eval scope "s = 'hello';")
|
||||
(is (= (js/get scope "s") "hello"))
|
||||
|
||||
(js/eval scope "o = {name: 'mariano'};")
|
||||
(is (= (js/get (js/get scope "o") "name") "mariano"))
|
||||
(is (= (js/get-in scope [:o :name]) "mariano"))
|
||||
|
||||
(js/eval scope "b = false")
|
||||
; TODO: this fails
|
||||
; (is (= (js/get-in scope [:b :name] :not-found) :not-found))
|
||||
(is (= (js/get scope "b") false))))
|
||||
)
|
Reference in a new issue