diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c33947eb..cbc380127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,14 +11,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Parameter and MappedParameter decorated properties from super classes are now inherited + by sub-classes [#646][646] + - Added `GITHUB_DEFAULT_REPO_VISIBILITY` mapped parameter that determines repository visibility based on organization GitHub plan - + ### Fixed - Ensure default values for optional parameters to Rug Functions are used, instead of null. Primitives are converted to 0 or false +[646]: https://github.com/atomist/rug/issues/646 + ## [1.0.0-m.5] - 2017-07-11 [1.0.0-m.5]: https://github.com/atomist/rug/compare/1.0.0-m.4...1.0.0-m.5 diff --git a/src/main/typescript/rug/operations/Decorators.ts b/src/main/typescript/rug/operations/Decorators.ts index 0ced1483c..f607544b9 100644 --- a/src/main/typescript/rug/operations/Decorators.ts +++ b/src/main/typescript/rug/operations/Decorators.ts @@ -17,11 +17,14 @@ function set_metadata(obj: any, key: string, value: any) { } function get_metadata(obj: any, key: string) { + if (obj == null) { + return null; + } 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) && (Object.getPrototypeOf(obj) !== undefined)) { + desc = get_metadata(Object.getPrototypeOf(obj), key); } - if (desc != null || desc !== undefined) { + if (desc != null && desc !== undefined) { return desc.value; } return null; @@ -37,14 +40,34 @@ export function Parameter(details: BaseParameter) { } export function declareParameter(target: any, propertyKey: string, details: BaseParameter) { - let params = get_metadata(target, "__parameters"); + let params: any[] = get_metadata(target, "__parameters"); if (params == null) { params = []; + } else { + // remove any that have the same name already (i.e. if folk are calling declareParameter) + // use a cheeky method so that we can reuse the same array + const found: any[] = params.filter((p) => p.name === propertyKey); + if (found != null && found.length > 0) { + const index = params.indexOf(found[0]); + params.splice(index, 1); + } } const copy: any = { ...details }; copy.name = propertyKey; copy.decorated = true; params.push(copy); + + // merge parameters from parent if it has some + const protoParams: any[] = get_metadata(Object.getPrototypeOf(target), "__parameters"); + if (protoParams != null) { + protoParams.forEach((protoParam) => { + // if we don't already have a parameter with the same name + if (!params.some((param) => param.name === protoParam.name)) { + params.push(protoParam); + } + }); + } + set_metadata(target, "__parameters", params); return target; } @@ -61,9 +84,29 @@ export function declareMappedParameter(target: any, localKey: string, foreignKey let params = get_metadata(target, "__mappedParameters"); if (params == null) { params = []; + } else { + // remove any that have the same name already (i.e. if folk are calling declareParameter) + // use a cheeky method so that we can reuse the same array + const found: any[] = params.filter((p) => p.localKey === localKey); + if (found != null && found.length > 0) { + const index = params.indexOf(found[0]); + params.splice(index, 1); + } } const param = { localKey, foreignKey }; params.push(param); + + // merge parameters from parent if it has some + const protoParams: any[] = get_metadata(Object.getPrototypeOf(target), "__mappedParameters"); + if (protoParams != null) { + protoParams.forEach((protoParam) => { + // if we don't already have a parameter with the same name + if (!params.some((p) => p.localKey === protoParam.localKey)) { + params.push(protoParam); + } + }); + } + set_metadata(target, "__mappedParameters", params); return target; } diff --git a/src/test/resources/com/atomist/rug/runtime/js/HandlerWithInherritedParameters.ts b/src/test/resources/com/atomist/rug/runtime/js/HandlerWithInherritedParameters.ts new file mode 100644 index 000000000..fb3efcba3 --- /dev/null +++ b/src/test/resources/com/atomist/rug/runtime/js/HandlerWithInherritedParameters.ts @@ -0,0 +1,54 @@ +import {HandleCommand, Instruction, Response, HandlerContext, CommandPlan, DirectedMessage, UserAddress} from '@atomist/rug/operations/Handlers' +import {CommandHandler, MappedParameter, declareMappedParameter, Parameter, declareParameter, Tags, Intent} from '@atomist/rug/operations/Decorators' +import {Build} from "@atomist/rug/cortex/stub/Build" + +let config = {description: "desc", pattern: "@any"} + +@CommandHandler("Uses same config object") +class ParentHandler implements HandleCommand{ + + @Parameter(config) + foo: number + + @Parameter(config) + bar: number + + @MappedParameter("blah-parent") + blah: string + + @MappedParameter("quz-parent") + quz: string + + handle(ctx: HandlerContext) { + return new CommandPlan(); + } +} + +export let parent = new ParentHandler(); + + +@CommandHandler("Child with overrides") +class ChildHandler extends ParentHandler { + + @Parameter({description: "child", pattern: "@any"}) + foo: number + + @Parameter(config) + baz: number + + @MappedParameter("blah-child") + blah: string + + @MappedParameter("really-child") + really: string + + handle(ctx: HandlerContext) { + return new CommandPlan(); + } +} +const child = new ChildHandler(); +declareParameter(child, "baz", {description: "dup", pattern: "@any"}) +declareMappedParameter(child, "really", "manual") + +export { child }; + diff --git a/src/test/scala/com/atomist/rug/runtime/js/JavaScriptUtilsTest.scala b/src/test/scala/com/atomist/rug/runtime/js/JavaScriptUtilsTest.scala new file mode 100644 index 000000000..9b39a611d --- /dev/null +++ b/src/test/scala/com/atomist/rug/runtime/js/JavaScriptUtilsTest.scala @@ -0,0 +1,45 @@ +package com.atomist.rug.runtime.js + +import com.atomist.project.archive.{AtomistConfig, DefaultAtomistConfig} +import com.atomist.rug.RugArchiveReader +import com.atomist.rug.TestUtils.contentOf +import com.atomist.rug.ts.TypeScriptBuilder +import com.atomist.source.{SimpleFileBasedArtifactSource, StringFileArtifact} +import org.scalatest.{FunSpec, Matchers} + +class JavaScriptUtilsTest extends FunSpec with Matchers { + + val atomistConfig: AtomistConfig = DefaultAtomistConfig + + val parameterInherritance = StringFileArtifact(atomistConfig.handlersRoot + "/Handler.ts", + contentOf(this, "HandlerWithInherritedParameters.ts")) + + it("should inherit parameters from the prototype") { + val rugArchive = TypeScriptBuilder.compileWithExtendedModel( + SimpleFileBasedArtifactSource(parameterInherritance)) + val rugs = RugArchiveReader(rugArchive) + val handler = rugs.commandHandlers.head + val params = handler.parameters + assert(params.size === 3) + assert(params(1).name === "foo") + assert(params(1).description === "child") + assert(params(2).name === "bar") + assert(params.head.name === "baz") + assert(params.head.description === "dup") + } + + it("should inherit mapped parameters from the prototype") { + val rugArchive = TypeScriptBuilder.compileWithExtendedModel( + SimpleFileBasedArtifactSource(parameterInherritance)) + val rugs = RugArchiveReader(rugArchive) + val handler = rugs.commandHandlers.head + val params = handler.mappedParameters + assert(params.size === 3) + assert(params.head.localKey === "really") + assert(params.head.foreignKey === "manual") + assert(params(1).localKey === "blah") + assert(params(1).foreignKey === "blah-child") + assert(params(2).localKey === "quz") + assert(params(2).foreignKey === "quz-parent") + } +}