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