commit 31a1807ec7f8b341a4d5334dc2f72898b719c931 Author: Mariano Guerra Date: Tue Jan 8 16:55:55 2013 +0100 initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee508f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/target +/lib +/classes +/checkouts +pom.xml +*.jar +*.class +.lein-deps-sum +.lein-failures +.lein-plugins diff --git a/README.rest b/README.rest new file mode 100644 index 0000000..6e378bf --- /dev/null +++ b/README.rest @@ -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") + # + 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'}") + # + user=> (js/get-in sc [:a :name]) + "spongebob" + user=> (js/get-in sc [:a :age]) + # + 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. diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..d30abb6 --- /dev/null +++ b/project.clj @@ -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"]]) diff --git a/src/clj_rhino.clj b/src/clj_rhino.clj new file mode 100644 index 0000000..995ed9c --- /dev/null +++ b/src/clj_rhino.clj @@ -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 "") + (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 "") + (or line-number 1) sec-domain)))) diff --git a/test/clj_rhino/core_test.clj b/test/clj_rhino/core_test.clj new file mode 100644 index 0000000..621481a --- /dev/null +++ b/test/clj_rhino/core_test.clj @@ -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)))) + )