diff --git a/.atomist.yml b/.atomist.yml index 8ce13de10..12b54b8a1 100644 --- a/.atomist.yml +++ b/.atomist.yml @@ -256,3 +256,18 @@ editor: parameters: - "class_under_test": "com.atomist.tree.content.text.microgrammar.MultipleUseOfSubmatcher" +--- +kind: "operation" +client: "rug-cli 1.0.0-m.2" +editor: + name: "AddTypeScriptTest" + group: "atomist" + artifact: "rug" + version: "0.3.0" + origin: + repo: "git@github.com:atomist/rug" + branch: "master" + sha: "fa808f2*" + parameters: + - "class_under_test": "com.atomist.rug.kind.PandaParsing" + diff --git a/.atomist/editors/AddTypeScriptTest.ts b/.atomist/editors/AddTypeScriptTest.ts index dadcbf96d..38e1458d9 100644 --- a/.atomist/editors/AddTypeScriptTest.ts +++ b/.atomist/editors/AddTypeScriptTest.ts @@ -29,8 +29,8 @@ class AddTypeScriptTest { let ts_file_path = "src/test/resources/" + ts_under_resources let scala_file_path = "src/test/scala/" + this.class_under_test.replace(/\./g, '/') + "TypeScriptTest.scala" - project.copyEditorBackingFileOrFail(sample_ts_file_path, ts_file_path) - project.copyEditorBackingFileOrFail("src/test/scala/com/atomist/rug/ts/SampleTypeScriptTest.scala", scala_file_path) + project.copyEditorBackingFileOrFailToDestination(sample_ts_file_path, ts_file_path) + project.copyEditorBackingFileOrFailToDestination("src/test/scala/com/atomist/rug/ts/SampleTypeScriptTest.scala", scala_file_path) let tsFile = project.findFile(ts_file_path) tsFile.replace("Sample", class_name) diff --git a/.atomist/manifest.yml b/.atomist/manifest.yml index 0e4c8984b..2aa27e557 100644 --- a/.atomist/manifest.yml +++ b/.atomist/manifest.yml @@ -1,6 +1,6 @@ group: atomist artifact: rug -version: "0.2.0" -requires: "[0.13.0,1.0.0)" +version: "0.3.0" +requires: "[1.0.0-m.3,2.0.0)" dependencies: extensions: diff --git a/src/test/resources/META-INF/services/com.atomist.rug.spi.Typed b/src/test/resources/META-INF/services/com.atomist.rug.spi.Typed index 0c8297fcc..0d538e7ce 100644 --- a/src/test/resources/META-INF/services/com.atomist.rug.spi.Typed +++ b/src/test/resources/META-INF/services/com.atomist.rug.spi.Typed @@ -1,4 +1,6 @@ com.atomist.rug.kind.test.ReplacerType -com.atomist.rug.kind.test.ReplacerCljType \ No newline at end of file +com.atomist.rug.kind.test.ReplacerCljType + +com.atomist.rug.kind.PandaRugLanguageExtension \ No newline at end of file diff --git a/src/test/resources/com/atomist/rug/kind/PandaParsingTypeScriptTest.ts b/src/test/resources/com/atomist/rug/kind/PandaParsingTypeScriptTest.ts new file mode 100644 index 000000000..f3d0588a9 --- /dev/null +++ b/src/test/resources/com/atomist/rug/kind/PandaParsingTypeScriptTest.ts @@ -0,0 +1,26 @@ +import { Project, File } from '@atomist/rug/model/Core' +import { Editor } from '@atomist/rug/operations/Decorators' +import { PathExpressionEngine, TextTreeNode } from '@atomist/rug/tree/PathExpression' +import { Parameter } from "@atomist/rug/operations/Decorators"; +import { Pattern } from "@atomist/rug/operations/RugOperation"; + +@Editor("PandaParsingTypeScriptTest", "Uses PandaParsing from TypeScript") +class PandaParsingTypeScriptTest { + + @Parameter({ pattern: Pattern.any }) + changePandasInCurliesTo: string; + + edit(project: Project) { + + let eng : PathExpressionEngine = project.context.pathExpressionEngine; + + let pandafile = eng.scalar(project, `/my.panda`); + let parsedPanda = eng.scalar(pandafile, "/Panda()"); + + eng.with(parsedPanda, `/curly/word`, pandaWord => { + pandaWord.update(this.changePandasInCurliesTo); + }) + + } +} +export let editor = new PandaParsingTypeScriptTest(); \ No newline at end of file diff --git a/src/test/resources/com/atomist/rug/runtime/js/interop/MutatingBanana.js b/src/test/resources/com/atomist/rug/runtime/js/interop/MutatingBanana.js deleted file mode 100644 index 72fd3ec02..000000000 --- a/src/test/resources/com/atomist/rug/runtime/js/interop/MutatingBanana.js +++ /dev/null @@ -1,77 +0,0 @@ -"use strict"; -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -exports.__esModule = true; -var Decorators_1 = require("@atomist/rug/operations/Decorators"); -var FruitererType = (function () { - function FruitererType() { - this.typeName = "fruiterer"; - } - FruitererType.prototype.isFile = function (f) { - return true; - }; - FruitererType.prototype.find = function (context) { - if (this.isFile(context)) { - var f = context; - if (f.isJava) - return [new Fruiterer(f)]; - else - return []; - } - else - return []; - }; - return FruitererType; -}()); -var Fruiterer = (function () { - function Fruiterer(file) { - this.file = file; - } - Fruiterer.prototype.parent = function () { return this.file; }; - Fruiterer.prototype.nodeName = function () { return "fruiterer"; }; - Fruiterer.prototype.nodeTags = function () { return [this.nodeName()]; }; - Fruiterer.prototype.children = function () { return [new MutatingBanana(this.file), new Pear()]; }; - return Fruiterer; -}()); -var MutatingBanana = (function () { - function MutatingBanana(file) { - this.file = file; - } - MutatingBanana.prototype.parent = function () { return this.file; }; - MutatingBanana.prototype.nodeName = function () { return "mutatingBanana"; }; - MutatingBanana.prototype.nodeTags = function () { return [this.nodeName()]; }; - MutatingBanana.prototype.children = function () { return []; }; - MutatingBanana.prototype.mutate = function () { - this.file.prepend("I am evil"); - }; - return MutatingBanana; -}()); -var Pear = (function () { - function Pear() { - } - Pear.prototype.parent = function () { return null; }; - Pear.prototype.nodeName = function () { return "pear"; }; - Pear.prototype.nodeTags = function () { return [this.nodeName()]; }; - Pear.prototype.children = function () { return []; }; - return Pear; -}()); -var TwoLevel = (function () { - function TwoLevel() { - } - TwoLevel.prototype.edit = function (project) { - var mg = new FruitererType(); - var eng = project.context.pathExpressionEngine.addType(mg); - eng["with"](project, "//File()/fruiterer()/mutatingBanana()", function (n) { - n.mutate(); - }); - }; - return TwoLevel; -}()); -TwoLevel = __decorate([ - Decorators_1.Editor("Constructed", "Uses two dynamic grammars") -], TwoLevel); -exports.editor = new TwoLevel(); diff --git a/src/test/resources/com/atomist/rug/runtime/js/interop/SimpleBanana.js b/src/test/resources/com/atomist/rug/runtime/js/interop/SimpleBanana.js deleted file mode 100644 index 3da78d692..000000000 --- a/src/test/resources/com/atomist/rug/runtime/js/interop/SimpleBanana.js +++ /dev/null @@ -1,60 +0,0 @@ -"use strict"; -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -exports.__esModule = true; -var Decorators_1 = require("@atomist/rug/operations/Decorators"); -var BananaType = (function () { - function BananaType() { - this.typeName = "banana"; - } - BananaType.prototype.find = function (context) { - return [new Banana()]; - }; - return BananaType; -}()); -var Banana = (function () { - function Banana() { - } - Banana.prototype.parent = function () { - return null; - }; - Banana.prototype.nodeName = function () { - return "banana"; - }; - Banana.prototype.nodeTags = function () { - return [this.nodeName()]; - }; - Banana.prototype.value = function () { - return "yellow"; - }; - Banana.prototype.children = function () { - return []; - }; - return Banana; -}()); -var SimpleBanana = (function () { - function SimpleBanana() { - } - SimpleBanana.prototype.edit = function (project) { - var mg = new BananaType(); - var eng = project.context.pathExpressionEngine.addType(mg); - var i = 0; - eng["with"](project, "//File()/banana()", function (n) { - //console.log("Checking color of banana") - if (n.value() != "yellow") - throw new Error("Banana is not yellow but [" + n.value() + "]. Sad."); - i++; - }); - if (i == 0) - throw new Error("No bananas tested. Sad."); - }; - return SimpleBanana; -}()); -SimpleBanana = __decorate([ - Decorators_1.Editor("Constructed", "Uses single dynamic grammar") -], SimpleBanana); -exports.editor = new SimpleBanana(); diff --git a/src/test/resources/com/atomist/rug/ts/SampleTypeScriptTest.ts b/src/test/resources/com/atomist/rug/ts/SampleTypeScriptTest.ts index cfa0487c0..eb847047b 100644 --- a/src/test/resources/com/atomist/rug/ts/SampleTypeScriptTest.ts +++ b/src/test/resources/com/atomist/rug/ts/SampleTypeScriptTest.ts @@ -1,6 +1,6 @@ -import { Project, File } from '@atomist/rug/model/Core' -import { Editor } from '@atomist/rug/operations/Decorators' -import { PathExpressionEngine } from '@atomist/rug/tree/PathExpression' +import { Project, File } from "@atomist/rug/model/Core" +import { Editor } from "@atomist/rug/operations/Decorators" +import { PathExpressionEngine } from "@atomist/rug/tree/PathExpression" @Editor("SampleTypeScriptTest", "Uses Sample from TypeScript") class SampleTypeScriptTest { diff --git a/src/test/scala/com/atomist/rug/kind/RugIsExtensible.scala b/src/test/scala/com/atomist/rug/kind/RugIsExtensible.scala new file mode 100644 index 000000000..04d7ab2b0 --- /dev/null +++ b/src/test/scala/com/atomist/rug/kind/RugIsExtensible.scala @@ -0,0 +1,185 @@ +package com.atomist.rug.kind + + +import com.atomist.rug.kind.grammar.TypeUnderFile +import org.scalatest.FunSpec + +import _root_.scala.util.parsing.combinator.RegexParsers + + +/** + * We can create Language Extensions for Rug + * and then use them to parse and navigate files. + * + * This test shows an extension with custom Scala parsing code. + */ + +class PandaRugLanguageExtension extends TypeUnderFile { + + import com.atomist.source.FileArtifact + import com.atomist.tree.content.text.PositionedTreeNode + + override val name: String = "Panda" // To trigger parsing, get to a file in the path expression, then /Panda() + + override def isOfType(f: FileArtifact): Boolean = f.name.endsWith(".panda") + + override def fileToRawNode(f: FileArtifact): Option[PositionedTreeNode] = + Some(PandaParser.parse(f.content)) + + override def description: String = "A language extension for my imaginary language" + +} + +object Panda { + val PandaText = + """panda { panda panda } panda + |(panda panda) panda + """.stripMargin + + val KawaiiPandas = + """panda { kawaii kawaii } panda + |(panda panda) panda + """.stripMargin + + val PandaFilename = "my.panda" + + import com.atomist.source.{SimpleFileBasedArtifactSource, StringFileArtifact} + + val PandaProject = SimpleFileBasedArtifactSource(StringFileArtifact(PandaFilename, PandaText)) + +} + +object PandaParser extends RegexParsers { + + import com.atomist.tree.content.text.{OffsetInputPosition, PositionedTreeNode} + + trait MinimalPositionedTreeNode extends PositionedTreeNode { + + import com.atomist.tree.TreeNode + + def name: String + + def valueOption: Option[String] + + def start: Int + + def end: Int + + override def nodeName = name + + override def startPosition = OffsetInputPosition(start) + + override def endPosition = OffsetInputPosition(end) + + override def value = valueOption.getOrElse("") + + override def childNodeNames: Set[String] = ??? + + override def childNodeTypes: Set[String] = ??? + + override def childrenNamed(key: String): Seq[TreeNode] = childNodes.filter(_.nodeName == key) + + } + + case class PositionedSyntaxNode(name: String, + override val childNodes: Seq[PositionedSyntaxNode], + override val valueOption: Option[String], + start: Int, + end: Int) extends MinimalPositionedTreeNode + + + case class SyntaxNode(name: String, + childNodes: Seq[PositionedSyntaxNode], + valueOption: Option[String]) + + object SyntaxNode { + + def leaf(name: String)(value: String): SyntaxNode = + SyntaxNode(name, Seq(), Some(value)) + + def parent(name: String, children: Seq[PositionedSyntaxNode]): SyntaxNode = + SyntaxNode(name, children, None) + } + + def positionedNode(inner: Parser[SyntaxNode]) = new Parser[PositionedSyntaxNode] { + override def apply(in: Input): ParseResult[PositionedSyntaxNode] = { + val start = handleWhiteSpace(in.source, in.offset) + inner.apply(in) match { + case Error(msg, next) => Error(msg, next) + case Failure(msg, next) => Failure(msg, next) + case Success(result, next) => + val end = next.offset + Success(PositionedSyntaxNode(result.name, result.childNodes, result.valueOption, + start, end), next) + } + } + } + + def curly: Parser[PositionedSyntaxNode] = + positionedNode("{" ~> rep(word) <~ "}" ^^ { n => SyntaxNode.parent("curly", n) }) + + def paren: Parser[PositionedSyntaxNode] = + positionedNode("(" ~> rep(word) <~ ")" ^^ { n => SyntaxNode.parent("paren", n) }) + + def word: Parser[PositionedSyntaxNode] = + positionedNode("[a-zA-Z]+".r ^^ SyntaxNode.leaf("word")) + + def pandaParser: Parser[PositionedSyntaxNode] = + positionedNode(rep(word | curly | paren) ^^ { parts => SyntaxNode.parent("panda", parts) }) + + def parse(content: String): PositionedSyntaxNode = { + parseAll(pandaParser, content) match { + case Success(result, next) => result + case z: NoSuccess => throw new IllegalArgumentException("Could not parse as panda: " + z) + } + } +} + +class RugIsExtensible extends FunSpec { + + describe("How rug can be extended to parse a panda language") { + + it("Can run an editor that uses the panda extension") { + + // TODO: get this Panda type declared in the manifest thinger somehow + + val changedProject = runTypeScriptProgram(Panda.PandaProject, + "com/atomist/rug/kind/PandaParsingTypeScriptTest.ts", + Map("changePandasInCurliesTo" -> "kawaii")) + + assert(changedProject.findFile(Panda.PandaFilename).get.content == Panda.KawaiiPandas) + + } + + } + + + import com.atomist.source.ArtifactSource + + def runTypeScriptProgram(target: ArtifactSource, tsEditorResource: String, parameterMap: Map[String, String]): ArtifactSource = { + + import com.atomist.param.SimpleParameterValues + import com.atomist.project.edit.SuccessfulModification + import com.atomist.rug.RugArchiveReader + import com.atomist.rug.ts.TypeScriptBuilder + import com.atomist.source.file.ClassPathArtifactSource + + val parameters = SimpleParameterValues.fromMap(parameterMap) + + // construct the Rug archive + val artifactSourceWithEditor = ClassPathArtifactSource.toArtifactSource(tsEditorResource).withPathAbove(".atomist/editors") + val artifactSourceWithRugNpmModule = TypeScriptBuilder.compileWithModel(artifactSourceWithEditor) + + // get the operation out of the artifact source + val projectEditor = RugArchiveReader(artifactSourceWithRugNpmModule).editors.head + + // apply the operation + projectEditor.modify(target, parameters) match { + case sm: SuccessfulModification => + sm.result + case boo => fail(s"Modification was not successful: $boo") + } + + } + +}