diff --git a/CHANGELOG.md b/CHANGELOG.md index 974c47eae..82f1b1ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Support for @parameter TS class field decorators as per + https://github.com/atomist/rug/issues/229 + - Support for a new TS (JS) Handler programming model as per https://github.com/atomist/rug/issues/105 @@ -36,6 +39,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed +- We now create a new JS rug for each thread for safety. + https://github.com/atomist/rug/issues/78 + - **BREAKING** all JS based Rugs must export (a la Common-JS) vars implementing the associated interfaces. Previously we scanned for all top level vars. diff --git a/src/main/scala/com/atomist/event/archive/HandlerArchiveReader.scala b/src/main/scala/com/atomist/event/archive/HandlerArchiveReader.scala index 0045263b3..e11ec15c3 100644 --- a/src/main/scala/com/atomist/event/archive/HandlerArchiveReader.scala +++ b/src/main/scala/com/atomist/event/archive/HandlerArchiveReader.scala @@ -42,7 +42,7 @@ class HandlerArchiveReader( if (handlers.nonEmpty) { handlers } else { - JavaScriptHandlerFinder.fromJavaScriptArchive(rugArchive, new JavaScriptHandlerContext(teamId, treeMaterializer, messageBuilder), None) + JavaScriptHandlerFinder.fromJavaScriptArchive(rugArchive, new JavaScriptHandlerContext(teamId, treeMaterializer, messageBuilder)) } } } diff --git a/src/main/scala/com/atomist/rug/runtime/js/JavaScriptContext.scala b/src/main/scala/com/atomist/rug/runtime/js/JavaScriptContext.scala index 109740c9f..a9f50ef95 100644 --- a/src/main/scala/com/atomist/rug/runtime/js/JavaScriptContext.scala +++ b/src/main/scala/com/atomist/rug/runtime/js/JavaScriptContext.scala @@ -1,14 +1,14 @@ package com.atomist.rug.runtime.js import java.util.regex.Pattern -import javax.script.{ScriptContext, ScriptException} +import javax.script._ import com.atomist.project.archive.{AtomistConfig, DefaultAtomistConfig} -import com.atomist.rug.{RugJavaScriptException, RugRuntimeException} +import com.atomist.rug.RugJavaScriptException import com.atomist.source.ArtifactSource import com.coveo.nashorn_modules.{AbstractFolder, Folder, Require} import com.typesafe.scalalogging.LazyLogging -import jdk.nashorn.api.scripting.{ClassFilter, NashornScriptEngine, NashornScriptEngineFactory, ScriptObjectMirror} +import jdk.nashorn.api.scripting.{NashornScriptEngine, NashornScriptEngineFactory, ScriptObjectMirror} import scala.collection.JavaConverters._ @@ -17,57 +17,65 @@ import scala.collection.JavaConverters._ * Creates a Nashorn ScriptEngineManager and can evaluate files and JavaScript fragments, * exposing the known vars in a typesafe way so we partly avoid the horrific detyped * Nashorn API. + * + * One of these per rug please, or else they may stomp on one-another */ -class JavaScriptContext(allowedClasses: Set[String] = Set.empty[String], atomistConfig: AtomistConfig = DefaultAtomistConfig) extends LazyLogging { +class JavaScriptContext(rugAs: ArtifactSource, + atomistConfig: AtomistConfig = DefaultAtomistConfig, + bindings: Bindings = new SimpleBindings()) extends LazyLogging { - private val commonOptions = Array("--optimistic-types", "--language=es6") - - /** - * At the time of writing, allowedClasses were only used for test. - * - * If you do need to expose some classes to JS, then make sure you configure to use a locked down classloader and security manager - */ val engine: NashornScriptEngine = - new NashornScriptEngineFactory().getScriptEngine( - if (allowedClasses.isEmpty) commonOptions :+ "--no-java" else commonOptions, - if (allowedClasses.isEmpty) null else Thread.currentThread().getContextClassLoader, //TODO - do we need our own loader here? - new ClassFilter { - override def exposeToScripts(s: String): Boolean = { - allowedClasses.contains(s) - } - } - ).asInstanceOf[NashornScriptEngine] - - def load(rugAs: ArtifactSource) : Unit = { - - configureEngine(engine, rugAs) - val filtered = atomistConfig.atomistContent(rugAs) - .filter(d => true, - f => atomistConfig.isJsSource(f)) - - //require all the atomist stuff - for (f <- filtered.allFiles) { - val varName = f.path.dropRight(3).replaceAll("/", "_").replaceAll("\\.", "\\$") - try { - engine.eval(s"exports.$varName = require('./${f.path.dropRight(3)}');") //because otherwise the loader doesn't know about the paths and can't resolve relative modules - } catch { - case x: ScriptException => throw new RugJavaScriptException(s"Error during eval of: ${f.path}",x) - case x: RuntimeException => x.getCause match { - case c: ScriptException => throw new RugJavaScriptException(s"Error during eval of: ${f.path}",c) - case c => throw x - } + new NashornScriptEngineFactory() + .getScriptEngine("--optimistic-types", "--language=es6", "--no-java") + .asInstanceOf[NashornScriptEngine] + + engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE) + + private val consoleJs = + """ + |console = { + | log: print, + | warn: print, + | error: print + |}; + """.stripMargin + + //so we can print stuff out from TS + engine.eval(consoleJs) + + try { + Require.enable(engine, new ArtifactSourceBasedFolder(rugAs)) + } catch { + case e: Exception => + throw new RuntimeException("Unable to set up ArtifactSource based module loader", e) + } + + private val filtered = atomistConfig.atomistContent(rugAs) + .filter(d => true, + f => atomistConfig.isJsSource(f)) + + //require all the atomist stuff + for (f <- filtered.allFiles) { + val varName = f.path.dropRight(3).replaceAll("/", "_").replaceAll("\\.", "\\$") + try { + engine.eval(s"exports.$varName = require('./${f.path.dropRight(3)}');") //because otherwise the loader doesn't know about the paths and can't resolve relative modules + } catch { + case x: ScriptException => throw new RugJavaScriptException(s"Error during eval of: ${f.path}", x) + case x: RuntimeException => x.getCause match { + case c: ScriptException => throw new RugJavaScriptException(s"Error during eval of: ${f.path}", c) + case c => throw x } } } + /** * Information about a JavaScript var exposed in the project scripts * * @param key name of the var * @param scriptObjectMirror interface for working with Var */ - case class Var(key: String, scriptObjectMirror: ScriptObjectMirror) { - } + case class Var(key: String, scriptObjectMirror: ScriptObjectMirror) {} /** * Return all the vars known to the engine that expose ScriptObjectMirror objects, with the key @@ -93,25 +101,6 @@ class JavaScriptContext(allowedClasses: Set[String] = Set.empty[String], atomist }).toSeq } - private def configureEngine(scriptEngine: NashornScriptEngine, rugAs: ArtifactSource): Unit = { - //so we can print stuff out from TS - val consoleJs = - """ - |console = { - | log: print, - | warn: print, - | error: print - |}; - """.stripMargin - scriptEngine.eval(consoleJs) - try{ - Require.enable(engine, new ArtifactSourceBasedFolder(rugAs)) - }catch { - case e: Exception => - throw new RuntimeException("Unable to set up ArtifactSource based module loader", e) - } - } - private class ArtifactSourceBasedFolder private(val artifacts: ArtifactSource, val parent: Folder, val path: String) extends AbstractFolder(parent, path) { private val commentPattern: Pattern = Pattern.compile("^//.*$", Pattern.MULTILINE) @@ -133,4 +122,5 @@ class JavaScriptContext(allowedClasses: Set[String] = Set.empty[String], atomist new ArtifactSourceBasedFolder(artifacts, this, getPath + s + "/") } } + } diff --git a/src/main/scala/com/atomist/rug/runtime/js/JavaScriptHandlerFinder.scala b/src/main/scala/com/atomist/rug/runtime/js/JavaScriptHandlerFinder.scala index 58133f8ef..51cc24732 100644 --- a/src/main/scala/com/atomist/rug/runtime/js/JavaScriptHandlerFinder.scala +++ b/src/main/scala/com/atomist/rug/runtime/js/JavaScriptHandlerFinder.scala @@ -1,5 +1,7 @@ package com.atomist.rug.runtime.js +import javax.script.SimpleBindings + import com.atomist.event.SystemEventHandler import com.atomist.param.Tag import com.atomist.project.archive.{AtomistConfig, DefaultAtomistConfig} @@ -16,7 +18,7 @@ import scala.util.Try object JavaScriptHandlerFinder { /** - * Find and handlers operations in the given Rug archive + * Find handler operations in the given Rug archive * * @param rugAs archive to look into * @param atomist facade to Atomist @@ -26,24 +28,18 @@ object JavaScriptHandlerFinder { def registerHandlers(rugAs: ArtifactSource, atomist: AtomistFacade, atomistConfig: AtomistConfig = DefaultAtomistConfig): Unit = { - val jsc = new JavaScriptContext() //TODO - remove this when new Handler model put in - jsc.engine.put("atomist", atomist) - jsc.load(rugAs) + val bindings = new SimpleBindings() + bindings.put("atomist", atomist) + new JavaScriptContext(rugAs,atomistConfig,bindings) } def fromJavaScriptArchive(rugAs: ArtifactSource, - ctx: JavaScriptHandlerContext, - context: Option[JavaScriptContext]): Seq[SystemEventHandler] = { + ctx: JavaScriptHandlerContext): Seq[SystemEventHandler] = { - val jsc: JavaScriptContext = - if (context.isEmpty) - new JavaScriptContext() - else - context.get + val jsc = new JavaScriptContext(rugAs) - jsc.load(rugAs) handlersFromVars(rugAs, jsc, ctx) } diff --git a/src/main/scala/com/atomist/rug/runtime/js/JavaScriptInvokingProjectOperation.scala b/src/main/scala/com/atomist/rug/runtime/js/JavaScriptInvokingProjectOperation.scala index b7771c455..6714093bb 100644 --- a/src/main/scala/com/atomist/rug/runtime/js/JavaScriptInvokingProjectOperation.scala +++ b/src/main/scala/com/atomist/rug/runtime/js/JavaScriptInvokingProjectOperation.scala @@ -1,6 +1,8 @@ package com.atomist.rug.runtime.js -import com.atomist.param.{AllowedValue, Parameter, Tag} +import javax.script.{ScriptContext, SimpleBindings} + +import com.atomist.param.{Parameter, Tag} import com.atomist.project.common.support.ProjectOperationParameterSupport import com.atomist.project.{ProjectOperation, ProjectOperationArguments} import com.atomist.rug.{InvalidRugParameterDefaultValue, InvalidRugParameterPatternException} @@ -12,7 +14,7 @@ import com.atomist.rug.runtime.rugdsl.ContextAwareProjectOperation import com.atomist.rug.spi.TypeRegistry import com.atomist.source.ArtifactSource import com.typesafe.scalalogging.LazyLogging -import jdk.nashorn.api.scripting.{ScriptObjectMirror, ScriptUtils} +import jdk.nashorn.api.scripting.ScriptObjectMirror import scala.collection.JavaConverters._ import scala.util.Try @@ -21,12 +23,12 @@ import scala.util.Try * Superclass for all operations that delegate to JavaScript. * * @param jsc JavaScript context - * @param jsVar var reference in Nashorn + * @param _jsVar var reference in Nashorn * @param rugAs backing artifact source for the Rug archive */ abstract class JavaScriptInvokingProjectOperation( jsc: JavaScriptContext, - jsVar: ScriptObjectMirror, + _jsVar: ScriptObjectMirror, rugAs: ArtifactSource ) extends ProjectOperationParameterSupport @@ -35,6 +37,9 @@ abstract class JavaScriptInvokingProjectOperation( private val typeRegistry: TypeRegistry = DefaultTypeRegistry + //visible for test + private[js] val jsVar = _jsVar + private val projectType = typeRegistry.findByName("Project") .getOrElse(throw new TypeNotPresentException("Project", null)) @@ -48,6 +53,8 @@ abstract class JavaScriptInvokingProjectOperation( _context = ctx } + + protected def context: Seq[ProjectOperation] = { _context } @@ -66,15 +73,60 @@ abstract class JavaScriptInvokingProjectOperation( * @return result of the invocation */ protected def invokeMemberWithParameters(member: String, args: Object*): Any = { + + val clone = cloneVar(jsVar) + // Translate parameters if necessary - val processedArgs = args.foldLeft(Seq[Object]())( - (acc: Seq[Object], cur: Object) => cur match { - case poa: ProjectOperationArguments => - acc :+ poa.parameterValues.map(p => p.getName -> p.getValue).toMap.asJava - case x => acc :+ x + val processedArgs = args.collect { + case poa: ProjectOperationArguments => { + val params = poa.parameterValues.map(p => p.getName -> p.getValue).toMap.asJava + setParamsIfDecorated(clone,params) + params + } + case x => x + } + + clone.asInstanceOf[ScriptObjectMirror].callMember(member,processedArgs: _* ) + } + + /** + * Separate for test + * @param jsVar + * @return + */ + private[js] def cloneVar (jsVar: ScriptObjectMirror) : ScriptObjectMirror = { + val bindings = new SimpleBindings() + bindings.put("rug",jsVar) + + //TODO - why do we need this? + jsc.engine.getContext.getBindings(ScriptContext.ENGINE_SCOPE).asScala.foreach{ + case (k: String, v: AnyRef) => bindings.put(k,v) + } + jsc.engine.eval("Object.create(rug);", bindings).asInstanceOf[ScriptObjectMirror] + } + /** + * Make sure we only set fields if they've been decorated with @parameter + * @param clone + * @param params + */ + private def setParamsIfDecorated(clone: ScriptObjectMirror, params: java.util.Map[String, AnyRef]): Unit = { + val decoratedParamNames: Set[String] = clone.get("parameters") match { + case ps: ScriptObjectMirror if !ps.isEmpty => { + ps.asScala.collect { + case (_, details: ScriptObjectMirror) if details.get("decorated").asInstanceOf[Boolean] => { + details.get("name").asInstanceOf[String] + } + }.toSet[String] + } + case _ => Set() + } + params.asScala.foreach { + case (k: String, v: AnyRef) => { + if(decoratedParamNames.contains(k)){ + clone.put(k,v) + } } - ) - jsVar.callMember(member,processedArgs: _* ) + } } protected def readTagsFromMetadata(someVar: ScriptObjectMirror): Seq[Tag] = { @@ -90,65 +142,71 @@ abstract class JavaScriptInvokingProjectOperation( }.getOrElse(Nil) } + /** + * Either read the parameters field or look for annotated parameters + * @return + */ protected def readParametersFromMetadata: Seq[Parameter] = { - - val pvar = jsVar.get("parameters").asInstanceOf[ScriptObjectMirror] - if(pvar == null || pvar.asScala.isEmpty){ - return Nil + jsVar.get("parameters") match { + case ps: ScriptObjectMirror if !ps.isEmpty => { + ps.asScala.collect { + case (_, details: ScriptObjectMirror) => parameterVarToParameter(jsVar, details) + }.toSeq + } + case _ => Seq() } - val values = pvar.asScala.collect { - case (_, _details: AnyRef) => - val details = _details.asInstanceOf[ScriptObjectMirror] - - val pName = details.get("name").asInstanceOf[String] - val pPattern = details.get("pattern").asInstanceOf[String] - val p = Parameter(pName, pPattern) - p.setDisplayName(details.get("displayName").asInstanceOf[String]) - - details.get("maxLength") match { - case x: AnyRef => p.setMaxLength(x.asInstanceOf[Int]) - case _ => p.setMaxLength(-1) - } - details.get("minLength") match { - case x: AnyRef => p.setMinLength(x.asInstanceOf[Int]) - case _ => p.setMinLength(-1) - } + } - p.setDefaultRef(details.get("defaultRef").asInstanceOf[String]) - val disp = details.get("displayable") - p.setDisplayable(if(disp != null) disp.asInstanceOf[Boolean] else true) - p.setRequired(details.get("required").asInstanceOf[Boolean]) + protected def parameterVarToParameter(rug: ScriptObjectMirror, details: ScriptObjectMirror) : Parameter = { - p.addTags(readTagsFromMetadata(details)) + val pName = details.get("name").asInstanceOf[String] + val pPattern = details.get("pattern").asInstanceOf[String] + val parameter = Parameter(pName, pPattern) + parameter.setDisplayName(details.get("displayName").asInstanceOf[String]) - p.setValidInputDescription(details.get("validInput").asInstanceOf[String]) - p.describedAs(details.get("description").asInstanceOf[String]) + details.get("maxLength") match { + case x: AnyRef => parameter.setMaxLength(x.asInstanceOf[Int]) + case _ => parameter.setMaxLength(-1) + } + details.get("minLength") match { + case x: AnyRef => parameter.setMinLength(x.asInstanceOf[Int]) + case _ => parameter.setMinLength(-1) + } - pPattern match { - case s: String if s.startsWith("@") => DefaultIdentifierResolver.resolve(s.substring(1)) match { - case Left(_) => - throw new InvalidRugParameterPatternException(s"Unable to recognize predefined validation pattern for parameter $pName: $s") - case Right(pat) => p.setPattern(pat) - } - case s: String if !s.startsWith("^") || !s.endsWith("$") => - throw new InvalidRugParameterPatternException(s"Parameter $pName validation pattern must contain anchors: $s") - case s: String => p.setPattern(s) - case _ => throw new InvalidRugParameterPatternException(s"Parameter $pName has no valid validation pattern") - } + parameter.setDefaultRef(details.get("defaultRef").asInstanceOf[String]) + val disp = details.get("displayable") + parameter.setDisplayable(if(disp != null) disp.asInstanceOf[Boolean] else true) + parameter.setRequired(details.get("required").asInstanceOf[Boolean]) - details.get("default") match { - case x: String => - if (!p.isValidValue(x)) - throw new InvalidRugParameterDefaultValue(s"Parameter $pName default value ($x) is not valid: $p") - p.setDefaultValue(x) - case _ => - } + parameter.addTags(readTagsFromMetadata(details)) + + parameter.setValidInputDescription(details.get("validInput").asInstanceOf[String]) + parameter.describedAs(details.get("description").asInstanceOf[String]) + + pPattern match { + case s: String if s.startsWith("@") => DefaultIdentifierResolver.resolve(s.substring(1)) match { + case Left(_) => + throw new InvalidRugParameterPatternException(s"Unable to recognize predefined validation pattern for parameter $pName: $s") + case Right(pat) => parameter.setPattern(pat) + } + case s: String if !s.startsWith("^") || !s.endsWith("$") => + throw new InvalidRugParameterPatternException(s"Parameter $pName validation pattern must contain anchors: $s") + case s: String => parameter.setPattern(s) + case _ => throw new InvalidRugParameterPatternException(s"Parameter $pName has no valid validation pattern") + } - p + details.get("default") match { + case x: String => + if (!parameter.isValidValue(x)) + throw new InvalidRugParameterDefaultValue(s"Parameter $pName default value ($x) is not valid: $parameter") + parameter.setDefaultValue(x) + case _ => + } + if(details.get("decorated").asInstanceOf[Boolean] && rug.hasMember(pName)){ + parameter.setDefaultValue(rug.getMember(pName).toString) } - values.toSeq + parameter } - /** * Convenient class allowing subclasses to wrap projects in a safe, updating proxy * @@ -158,5 +216,4 @@ abstract class JavaScriptInvokingProjectOperation( protected def wrapProject(pmv: ProjectMutableView): jsSafeCommittingProxy = { new jsSafeCommittingProxy(projectType, pmv) } - } diff --git a/src/main/scala/com/atomist/rug/runtime/js/JavaScriptOperationFinder.scala b/src/main/scala/com/atomist/rug/runtime/js/JavaScriptOperationFinder.scala index 0638ce408..a0b6268aa 100644 --- a/src/main/scala/com/atomist/rug/runtime/js/JavaScriptOperationFinder.scala +++ b/src/main/scala/com/atomist/rug/runtime/js/JavaScriptOperationFinder.scala @@ -33,16 +33,8 @@ object JavaScriptOperationFinder { * @param rugAs archive to look into * @return a sequence of instantiated operations backed by JavaScript */ - def fromJavaScriptArchive(rugAs: ArtifactSource, - context: JavaScriptContext = null): Seq[ProjectOperation] = { - val jsc: JavaScriptContext = - if (context == null) - new JavaScriptContext() - else - context - - jsc.load(rugAs) - operationsFromVars(rugAs, jsc) + def fromJavaScriptArchive(rugAs: ArtifactSource): Seq[ProjectOperation] = { + operationsFromVars(rugAs, new JavaScriptContext(rugAs)) } // TODO clean up this dispatch/signature stuff - too coupled diff --git a/src/main/typescript/node_modules/@atomist/rug/operations/RugOperation.ts b/src/main/typescript/node_modules/@atomist/rug/operations/RugOperation.ts index 4b10f0c62..ca84d597b 100644 --- a/src/main/typescript/node_modules/@atomist/rug/operations/RugOperation.ts +++ b/src/main/typescript/node_modules/@atomist/rug/operations/RugOperation.ts @@ -30,20 +30,24 @@ abstract class Pattern { public static uuid: string ="@uuid" } -interface Parameter { +interface BaseParameter { + pattern: string required?: boolean - name: string description?: string displayName?: string validInput?: string displayable?: boolean - default?: string - pattern: string maxLength?: number minLength?: number tags?: string[] } +interface Parameter extends BaseParameter{ + name: string + default?: string +} + + /** * Status of an operation. */ @@ -93,4 +97,41 @@ class ReviewResult { public comments: ReviewComment[]) {} } -export {RugOperation, Parameter, Pattern, Result, Status, ReviewResult, ReviewComment, Severity} + + +//used by annotation functions + +function set_metadata(obj: any, key: string, value: any){ + Object.defineProperty(obj, key, {value: value, writable: false, enumerable: false}) +} + + +function get_metadata(obj: any, key: string){ + let desc = Object.getOwnPropertyDescriptor(obj, key); + if((desc == null || desc == undefined) && (obj.prototype != undefined)){ + desc = Object.getOwnPropertyDescriptor(obj.prototype, key); + } + if(desc != null || desc != undefined){ + return desc.value; + } + return null; +} + + +/** +* Decorator for parameters. Adds to object properties +*/ +function parameter(details: BaseParameter) { + return function (target: any, propertyKey: string) { + var params = get_metadata(target, "parameters"); + if(params == null){ + params = [] + } + details["name"] = propertyKey + details["decorated"] = true + params.push(details); + set_metadata(target, "parameters", params); + } +} + +export {RugOperation, Parameter, Pattern, Result, Status, ReviewResult, ReviewComment, Severity, BaseParameter, parameter} diff --git a/src/test/scala/com/atomist/rug/kind/elm/ElmTypeScriptEditorTest.scala b/src/test/scala/com/atomist/rug/kind/elm/ElmTypeScriptEditorTest.scala index b1311278e..d0649f6a6 100644 --- a/src/test/scala/com/atomist/rug/kind/elm/ElmTypeScriptEditorTest.scala +++ b/src/test/scala/com/atomist/rug/kind/elm/ElmTypeScriptEditorTest.scala @@ -27,12 +27,8 @@ class ElmTypeScriptEditorTest extends FlatSpec with Matchers { withClue(s"README content----------\n$readme\n----------\n") { readme.contains( s"# ${projectName}") should be(true) - readme.contains(s"${System.lineSeparator()}${description}${System.lineSeparator()}") should be(true) - - // readme.contains(s"https://${org}.github.io/${repo}") should be(true) } - } def singleFileArtifactSource(projectName: String): SimpleFileBasedArtifactSource = { diff --git a/src/test/scala/com/atomist/rug/runtime/HandlerTest.scala b/src/test/scala/com/atomist/rug/runtime/HandlerTest.scala index 218d2a161..639c3851c 100644 --- a/src/test/scala/com/atomist/rug/runtime/HandlerTest.scala +++ b/src/test/scala/com/atomist/rug/runtime/HandlerTest.scala @@ -1,11 +1,12 @@ package com.atomist.rug.runtime import java.util.Collections +import javax.script.SimpleBindings -import com.atomist.rug.TestUtils +import com.atomist.project.archive.DefaultAtomistConfig import com.atomist.rug.kind.service.{ConsoleMessageBuilder, EmptyActionRegistry} -import com.atomist.rug.runtime.js.JavaScriptContext -import com.atomist.rug.runtime.js.interop.{AtomistFacade, NamedJavaScriptEventHandlerTest, jsMatch, jsPathExpressionEngine} +import com.atomist.rug.runtime.js.{JavaScriptContext, JavaScriptHandlerFinder} +import com.atomist.rug.runtime.js.interop._ import com.atomist.rug.ts.TypeScriptBuilder import com.atomist.source.{SimpleFileBasedArtifactSource, StringFileArtifact} import com.atomist.tree.SimpleTerminalTreeNode @@ -18,29 +19,29 @@ class HandlerTest extends FlatSpec with Matchers { val subscription = s""" - |import {Atomist} from "@atomist/rug/operations/Handler" - |import {Project,File} from "@atomist/rug/model/Core" - | + |import {Atomist} from "@atomist/rug/operations/Handler" + |import {Project,File} from "@atomist/rug/model/Core" + | |declare var atomist: Atomist // <= this is for the compiler only - | + | |declare var print: any - | + | |atomist.messageBuilder().say("This is a test").on("channel").send() - | + | |atomist.on('/src/main//*.java', m => { - | //print(`in handler with $${m}`) - | //print(`Root=$${m.root()}, leaves=$${m.matches()}`) - |}) + | //print(`in handler with $${m}`) + | //print(`Root=$${m.root()}, leaves=$${m.matches()}`) + |}) """.stripMargin val r = TypeScriptBuilder.compileWithModel(SimpleFileBasedArtifactSource( StringFileArtifact(".atomist/handlers/sub1.ts", subscription) )) - val jsc = new JavaScriptContext() + val bindings = new SimpleBindings() + bindings.put("atomist", TestAtomistFacade) + val jsc = new JavaScriptContext(r, DefaultAtomistConfig, bindings) - jsc.engine.put("atomist", TestAtomistFacade) - jsc.load(r) for (ts <- r.allFiles.filter(_.name.endsWith(".js"))) { //TODO - call compiler //jsc.eval(ts) @@ -49,12 +50,10 @@ class HandlerTest extends FlatSpec with Matchers { } it should "find and invoke other style of handler" in { - - val r = TypeScriptBuilder.compileWithModel(SimpleFileBasedArtifactSource(NamedJavaScriptEventHandlerTest.reOpenCloseIssueProgram, NamedJavaScriptEventHandlerTest.issuesStuff)) - val jsc = new JavaScriptContext() - - jsc.load(r) - } + val r = TypeScriptBuilder.compileWithModel(SimpleFileBasedArtifactSource(NamedJavaScriptEventHandlerTest.reOpenCloseIssueProgram, NamedJavaScriptEventHandlerTest.issuesStuff)) + val ctx = new JavaScriptHandlerContext(null,null,null) + JavaScriptHandlerFinder.fromJavaScriptArchive(r,ctx) + } } object TestAtomistFacade extends AtomistFacade { @@ -66,7 +65,7 @@ object TestAtomistFacade extends AtomistFacade { case som: ScriptObjectMirror => val arg = jsMatch(SimpleTerminalTreeNode("root", "x"), Collections.emptyList()) val args = Seq(arg) - som.call("apply", args:_*) + som.call("apply", args: _*) } } diff --git a/src/test/scala/com/atomist/rug/runtime/js/JavaScriptContextTest.scala b/src/test/scala/com/atomist/rug/runtime/js/JavaScriptContextTest.scala index 319ad39f7..2ba21c05c 100644 --- a/src/test/scala/com/atomist/rug/runtime/js/JavaScriptContextTest.scala +++ b/src/test/scala/com/atomist/rug/runtime/js/JavaScriptContextTest.scala @@ -13,7 +13,7 @@ class JavaScriptContextTest extends FlatSpec with Matchers { it should "throw an exception containing the JS file name if there are error during eval" in { val filename = ".atomist/editors/SimpleEditor.js" val caught = intercept[RugJavaScriptException] { - new JavaScriptContext().load(SimpleFileBasedArtifactSource(StringFileArtifact(filename, SimpleEditorWithoutParameters))) + new JavaScriptContext(SimpleFileBasedArtifactSource(StringFileArtifact(filename, SimpleEditorWithoutParameters))) } caught.getMessage should include(filename) } diff --git a/src/test/scala/com/atomist/rug/runtime/js/JavaScriptInvokingProjectOperationTest.scala b/src/test/scala/com/atomist/rug/runtime/js/JavaScriptInvokingProjectOperationTest.scala index 01fad59da..7cf9c0b6e 100644 --- a/src/test/scala/com/atomist/rug/runtime/js/JavaScriptInvokingProjectOperationTest.scala +++ b/src/test/scala/com/atomist/rug/runtime/js/JavaScriptInvokingProjectOperationTest.scala @@ -207,6 +207,15 @@ class JavaScriptInvokingProjectOperationTest extends FlatSpec with Matchers { } } + it should "create two separate js objects for each operation" in { + val tsf = StringFileArtifact(s".atomist/reviewers/SimpleEditor.ts", SimpleEditorInvokingOtherEditorAndAddingToOurOwnParameters) + val as = TypeScriptBuilder.compileWithModel(SimpleFileBasedArtifactSource(tsf)) + val jsed = JavaScriptOperationFinder.fromJavaScriptArchive(as).head.asInstanceOf[JavaScriptInvokingProjectEditor] + val v1 = jsed.cloneVar(jsed.jsVar) + v1.put("name", "dude") + jsed.jsVar.get("name") should be ("Simple") + } + private def invokeAndVerifySimpleEditor(tsf: FileArtifact): JavaScriptInvokingProjectEditor = { val as = TypeScriptBuilder.compileWithModel(SimpleFileBasedArtifactSource(tsf)) val jsed = JavaScriptOperationFinder.fromJavaScriptArchive(as).head.asInstanceOf[JavaScriptInvokingProjectEditor] diff --git a/src/test/scala/com/atomist/rug/runtime/js/JavaScriptOperationFinderTest.scala b/src/test/scala/com/atomist/rug/runtime/js/JavaScriptOperationFinderTest.scala new file mode 100644 index 000000000..487b3eea1 --- /dev/null +++ b/src/test/scala/com/atomist/rug/runtime/js/JavaScriptOperationFinderTest.scala @@ -0,0 +1,132 @@ +package com.atomist.rug.runtime.js + +import com.atomist.project.SimpleProjectOperationArguments +import com.atomist.rug.ts.TypeScriptBuilder +import com.atomist.source.{FileArtifact, SimpleFileBasedArtifactSource, StringFileArtifact} +import org.scalatest.{FlatSpec, Matchers} +import com.atomist.util.Timing._ + + +class JavaScriptOperationFinderTest extends FlatSpec with Matchers { + + val SimpleProjectEditorWithParametersArray: String = + s""" + |import {Project} from '@atomist/rug/model/Core' + |import {ProjectEditor} from '@atomist/rug/operations/ProjectEditor' + |import {File} from '@atomist/rug/model/Core' + |import {Parameter} from '@atomist/rug/operations/RugOperation' + | + |class SimpleEditor implements ProjectEditor { + | name: string = "Simple" + | description: string = "A nice little editor" + | parameters: Parameter[] = [{name: "content", description: "Content", pattern: "^.*$$", maxLength: 100}] + | edit(project: Project, {content} : {content: string}) { + | } + | } + |export let editor = new SimpleEditor() + """.stripMargin + + val SimpleProjectEditorWithAnnotatedParameters: String = + s""" + |import {Project} from '@atomist/rug/model/Core' + |import {ProjectEditor} from '@atomist/rug/operations/ProjectEditor' + |import {File} from '@atomist/rug/model/Core' + |import {parameter} from '@atomist/rug/operations/RugOperation' + | + |class SimpleEditor implements ProjectEditor { + | name: string = "Simple" + | description: string = "A nice little editor" + | + | @parameter({pattern: "^.*$$", description: "foo bar"}) + | content: string = "Test String"; + | + | @parameter({pattern: "^\\d+$$", description: "A nice round number"}) + | amount: number = 10; + | + | @parameter({pattern: "^\\d+$$", description: "A nice round number"}) + | nope: boolean; + | + | edit(project: Project) { + | if(this.amount != 10) { + | throw new Error("Number should be 10!"); + | } + | if(this.content != "woot") { + | throw new Error("Name should be woot"); + | } + | } + | } + |export let editor = new SimpleEditor() + """.stripMargin + + it should "find an editor with a parameters list" in { + val eds = invokeAndVerifySimple(StringFileArtifact(s".atomist/editors/SimpleEditor.ts", SimpleProjectEditorWithParametersArray)) + eds.parameters.size should be(1) + } + + it should "find an editor using annotated parameters" in { + val eds = invokeAndVerifySimple(StringFileArtifact(s".atomist/editors/SimpleEditor.ts", SimpleProjectEditorWithAnnotatedParameters)) + eds.parameters.size should be(3) + eds.parameters(0).getDefaultValue should be("Test String") + eds.parameters(1).getDefaultValue should be("10") + eds.parameters(2).getDefaultValue should be("") + + } + + it should "run fast, especially when run a whole bunch of times" in { + //runPerfTest() + } + + private def runPerfTest(): Unit = { + val target = SimpleFileBasedArtifactSource(StringFileArtifact("pom.xml", "nasty stuff")) + val (as, compileTime) = time { + val tsf = StringFileArtifact(s".atomist/editors/SimpleEditor.ts", SimpleProjectEditorWithAnnotatedParameters) + TypeScriptBuilder.compileWithModel(SimpleFileBasedArtifactSource(tsf)) + } + println(s"Compiling took: $compileTime ms") + + val (ed, evalTime) = time { + JavaScriptOperationFinder.fromJavaScriptArchive(as).head.asInstanceOf[JavaScriptInvokingProjectEditor] + } + println(s"Loading editor took: $evalTime ms") + + val (_, run1) = time { + 1 to 2 foreach { _ => ed.modify(target,SimpleProjectOperationArguments("", Map("content" -> "woot")))} + } + println(s"1 run took: -> $run1 ms") + + val (_, run10) = time { + 1 to 10 foreach { _ => ed.modify(target,SimpleProjectOperationArguments("", Map("content" -> "woot")))} + } + println(s"10 runs took: -> ${run10/10d} ms/run") + + val (_, run100) = time { + 1 to 100 foreach { _ => ed.modify(target,SimpleProjectOperationArguments("", Map("content" -> "woot")))} + } + println(s"100 runs took: -> ${run100/100d} ms/run") + + val (_, run1000) = time { + 1 to 1000 foreach { _ => ed.modify(target,SimpleProjectOperationArguments("", Map("content" -> "woot")))} + } + println(s"1000 runs took: -> ${run1000/1000d} ms/run") + + val (_, run100000) = time { + 1 to 100000 foreach { _ => ed.modify(target,SimpleProjectOperationArguments("", Map("content" -> "woot")))} + } + println(s"100000 runs took: -> ${run100000/100000d} ms/run") + + val (_, run1000000) = time { + 1 to 1000000 foreach { _ => ed.modify(target,SimpleProjectOperationArguments("", Map("content" -> "woot")))} + } + println(s"1000000 runs took: -> ${run1000000/1000000d} ms/run") + } + + + private def invokeAndVerifySimple(tsf: FileArtifact): JavaScriptInvokingProjectEditor = { + val as = TypeScriptBuilder.compileWithModel(SimpleFileBasedArtifactSource(tsf)) + val jsed = JavaScriptOperationFinder.fromJavaScriptArchive(as).head.asInstanceOf[JavaScriptInvokingProjectEditor] + jsed.name should be("Simple") + val target = SimpleFileBasedArtifactSource(StringFileArtifact("pom.xml", "nasty stuff")) + jsed.modify(target,SimpleProjectOperationArguments("", Map("content" -> "woot"))) + jsed + } +} diff --git a/src/test/scala/com/atomist/rug/ts/TypeScriptBuilder.scala b/src/test/scala/com/atomist/rug/ts/TypeScriptBuilder.scala index dbd0f634b..74b68f797 100644 --- a/src/test/scala/com/atomist/rug/ts/TypeScriptBuilder.scala +++ b/src/test/scala/com/atomist/rug/ts/TypeScriptBuilder.scala @@ -6,6 +6,7 @@ import com.atomist.project.SimpleProjectOperationArguments import com.atomist.rug.compiler.typescript.TypeScriptCompiler import com.atomist.source.ArtifactSource import com.atomist.source.file.{FileSystemArtifactSource, FileSystemArtifactSourceIdentifier} +import com.atomist.source.filter.ArtifactFilter /** * Helps us compile TypeScript archives. @@ -18,7 +19,9 @@ object TypeScriptBuilder { val userModel: ArtifactSource = { val generator = new TypeScriptInterfaceGenerator val output = generator.generate("stuff", SimpleProjectOperationArguments("", Map(generator.OutputPathParam -> "Core.ts"))) - val src = new FileSystemArtifactSource(FileSystemArtifactSourceIdentifier(new File("src/main/typescript"))) // THIS ONLY WORKS IN TESTS NOT IN PRODUCTION + val src = new FileSystemArtifactSource(FileSystemArtifactSourceIdentifier(new File("src/main/typescript")), new ArtifactFilter { + override def apply(s: String) = {!s.endsWith(".js")} + }) // THIS ONLY WORKS IN TESTS NOT IN PRODUCTION BY DESIGN val compiled = compiler.compile(src.underPath("node_modules/@atomist").withPathAbove(".atomist") + output.withPathAbove(".atomist/rug/model")) compiled.underPath(".atomist").withPathAbove(".atomist/node_modules/@atomist") } diff --git a/src/test/scala/com/atomist/util/lang/JavaScriptArrayTest.scala b/src/test/scala/com/atomist/util/lang/JavaScriptArrayTest.scala index e0897fac5..fc22ca1b8 100644 --- a/src/test/scala/com/atomist/util/lang/JavaScriptArrayTest.scala +++ b/src/test/scala/com/atomist/util/lang/JavaScriptArrayTest.scala @@ -20,23 +20,14 @@ class JavaScriptArrayTest extends FlatSpec with Matchers { | |import {Result,Status, Parameter} from '@atomist/rug/operations/RugOperation' | - | - |declare var Java - | - |var ArrayList = Java.type("java.util.ArrayList"); - |var jlist = new ArrayList; - |jlist.add("blah"); - |var FancyList = Java.type("com.atomist.util.lang.JavaScriptArray"); - |var javaList = new FancyList(jlist); - | |class ConstructedEditor implements ProjectEditor { | name: string = "Constructed" | description: string = "A nice little editor" | parameters: Parameter[] = [{name: "packageName", description: "The java package name", displayName: "Java Package", pattern: "^.*$", maxLength: 100}] | lyst: string[] - | edit(project: Project, {packageName}: {packageName: string}) { + | edit(project: Project, {packageName, strings}: {packageName: string, strings: string[]}) { | - | this.lyst = javaList as string[]; + | this.lyst = strings | | this.lyst[0].toString() | @@ -246,26 +237,17 @@ class JavaScriptArrayTest extends FlatSpec with Matchers { private def invokeAndVerifyConstructed(tsf: FileArtifact): JavaScriptInvokingProjectEditor = { val as = TypeScriptBuilder.compileWithModel(SimpleFileBasedArtifactSource(tsf)) - val ctx = new JavaScriptContext(Set("java.util.ArrayList","com.atomist.util.lang.JavaScriptArray")) - ctx.load(as) - val jsed = JavaScriptOperationFinder.fromJavaScriptArchive(as, ctx).head.asInstanceOf[JavaScriptInvokingProjectEditor] + val jsed = JavaScriptOperationFinder.fromJavaScriptArchive(as).head.asInstanceOf[JavaScriptInvokingProjectEditor] jsed.name should be("Constructed") val target = SimpleFileBasedArtifactSource(StringFileArtifact("pom.xml", "nasty stuff")) - jsed.modify(target, SimpleProjectOperationArguments("", Map("packageName" -> "com.atomist.crushed"))) match { + val lyzt = new util.ArrayList[String]() + lyzt.add("blah") + jsed.modify(target, SimpleProjectOperationArguments("", Map("packageName" -> "com.atomist.crushed", "strings" -> new JavaScriptArray(lyzt)))) match { case sm: NoModificationNeeded => sm.comment.contains("OK") should be(true) } jsed } - - object TemporaryRegistry extends UserModelContext{ - - val lyst = new util.ArrayList[String]() - lyst.add("blah") - override val registry = Map( - "PathExpressionEngine" -> new JavaScriptArray[String](lyst) - ) - } }