diff --git a/api-cli/package.json b/api-cli/package.json index e6bbf57cd4..5d8e64c3dc 100644 --- a/api-cli/package.json +++ b/api-cli/package.json @@ -1,7 +1,7 @@ { "name": "@useoptic/cli", "description": "Optic's open source tool for building APIs that document & test themselves", - "version": "6.5.0", + "version": "6.5.1", "author": "@useoptic", "bin": { "api": "./bin/run" diff --git a/api-cli/src/Paths.ts b/api-cli/src/Paths.ts index 3a693c2fd5..41879fd7ce 100644 --- a/api-cli/src/Paths.ts +++ b/api-cli/src/Paths.ts @@ -3,7 +3,7 @@ import * as findUp from 'find-up' import * as fs from 'fs-extra' export interface IPathMapping { - + } export async function getPaths(fallbackPath: (cwd: string) => string = (cwd) => path.join(cwd, '.api')) { const rootPath = await (async () => { @@ -13,7 +13,7 @@ export async function getPaths(fallbackPath: (cwd: string) => string = (cwd) => } return fallbackPath(process.cwd()) })() - + await fs.ensureDir(rootPath) process.chdir(path.resolve(rootPath, '../')) @@ -31,7 +31,7 @@ async function getPathsRelativeToCwd(cwd: string): Promise { const exampleRequestsPath = path.join(basePath, 'example-requests') await fs.ensureDir(sessionsPath) await fs.ensureDir(exampleRequestsPath) - const outputPath = path.join(basePath, 'output') + const outputPath = path.join(basePath, 'generated') return { cwd, @@ -44,4 +44,4 @@ async function getPathsRelativeToCwd(cwd: string): Promise { exampleRequestsPath, outputPath } -} \ No newline at end of file +} diff --git a/api-cli/src/commands/generate.ts b/api-cli/src/commands/generate.ts deleted file mode 100644 index ad9d8c23b5..0000000000 --- a/api-cli/src/commands/generate.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {Command, flags} from '@oclif/command' -import * as fs from 'fs-extra' -import * as clipboardy from 'clipboardy' -// @ts-ignore -import * as niceTry from 'nice-try' -import * as path from 'path' -// @ts-ignore -import cli from 'cli-ux' -// @ts-ignore -import * as fetch from 'node-fetch' -import {getPaths} from '../Paths' -import {prepareEvents} from '../PersistUtils' -import * as yaml from 'js-yaml' -import analytics from '../lib/analytics' - -export default class Generate extends Command { - - static description = 'generate something useful from your API spec' - - static flags = { - // output: flags.string() - } - - static args = [ - {name: 'buildId', description: 'what do you want to generate?', required: true} - ] - - async run() { - const {flags, args} = this.parse(Generate) - const {buildId} = args - - if (flags.output) { - analytics.track('init from web') - } - // @ts-ignore - const {outputPath, specStorePath} = await getPaths() - - if (buildId === 'oas') { - - const fileContents = niceTry(() => fs.readFileSync(specStorePath).toString()) || '[]' - cli.action.start('Generating OAS file') - - // @ts-ignore - const response = await fetch('https://ayiz1s0f8f.execute-api.us-east-2.amazonaws.com/production/oas/generate', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({fileContents}) - }) - - if (response.status === 200) { - const oasJson = await response.json() - fs.ensureDirSync(outputPath) - cli.action.stop('done') - const oasPath = path.join(outputPath, 'oas.json') - fs.writeFileSync(path.join(outputPath, 'oas.json'), JSON.stringify(oasJson, null, 2)) - this.log('Writing OAS file to: ' + oasPath) - return oasPath - - } else { - return this.error('OAS Export Error' + await response.text()) - } - } else { - this.error(`No build exists for ${buildId}`) - } - - } -} diff --git a/api-cli/src/commands/generate/oas.ts b/api-cli/src/commands/generate/oas.ts new file mode 100644 index 0000000000..c35f3747ba --- /dev/null +++ b/api-cli/src/commands/generate/oas.ts @@ -0,0 +1,65 @@ +import {Command, flags} from '@oclif/command' +import * as fs from 'fs-extra' +import * as clipboardy from 'clipboardy' +// @ts-ignore +import * as niceTry from 'nice-try' +import * as path from 'path' +// @ts-ignore +import * as opticEngine from '../../../provided/domain.js' +import cli from 'cli-ux' +// @ts-ignore +import * as fetch from 'node-fetch' +import {getPaths} from '../../Paths' +import {prepareEvents} from '../../PersistUtils' +import analytics from '../../lib/analytics' +import * as yaml from 'js-yaml' + + +export default class GenerateOas extends Command { + + static description = 'export an OpenAPI 3.1 spec' + + static flags = { + json: flags.boolean(), + yaml: flags.boolean(), + } + + async run() { + + const {flags} = this.parse(GenerateOas) + // @ts-ignore + const {outputPath, specStorePath} = await getPaths() + + const oasGenerator = opticEngine.com.seamless.contexts.rfc.projections.OASProjectionHelper() + + const fileContents = niceTry(() => fs.readFileSync(specStorePath).toString()) || '[]' + cli.action.start('Generating OAS file') + + try { + const specAsJson = oasGenerator.fromEventString(fileContents) + + const writeJson = flags.json || (!flags.json && !flags.yaml) + const writeYaml = flags.yaml + + cli.action.stop('Done!') + + if (writeJson) { + fs.ensureDirSync(outputPath) + const oasPath = path.join(outputPath, 'openapi.json') + fs.writeFileSync(path.join(outputPath, 'openapi.json'), JSON.stringify(specAsJson, null, 2)) + return this.log('OpenAPI written to ' + oasPath) + } + + if (writeYaml) { + fs.ensureDirSync(outputPath) + const oasPath = path.join(outputPath, 'openapi.yaml') + fs.writeFileSync(path.join(outputPath, 'openapi.yaml'), yaml.safeDump(specAsJson, {indent: 1})) + return this.log('OpenAPI written to ' + oasPath) + } + + } catch (e) { + console.error('Error generating OpenAPI ') + console.log(e) + } + } +} diff --git a/api-cli/src/commands/publish-oas.ts b/api-cli/src/commands/publish-oas.ts index 541d221b85..269be9fa94 100644 --- a/api-cli/src/commands/publish-oas.ts +++ b/api-cli/src/commands/publish-oas.ts @@ -10,7 +10,7 @@ import cli from 'cli-ux' import * as fetch from 'node-fetch' import {fromOptic} from '../lib/log-helper' import * as colors from 'colors' -import Generate from './generate' +import Oas from './generate/oas' import {exec, spawn, SpawnOptions} from 'child_process' import Init from './init' import {processSetting, readApiConfig} from './start' @@ -37,7 +37,7 @@ export default class PublishOas extends Command { return this.error('No command registered for `publish-oas`. Add one to your api.yml file') } - const generated = await Generate.run(['oas']) + const generated = await Oas.run(['oas']) if (generated) { console.log(generated) const commandToRun = processSetting(publishOasCommand, {OAS_PATH: generated}) diff --git a/api-cli/tsconfig.tsbuildinfo b/api-cli/tsconfig.tsbuildinfo index be0455c479..c268293a6d 100644 --- a/api-cli/tsconfig.tsbuildinfo +++ b/api-cli/tsconfig.tsbuildinfo @@ -326,7 +326,7 @@ "signature": "aca36e2d27783f4bad7fc1786a532ff76024f0fc8575df48bcd9a5eb452fe7e7" }, "./src/paths.ts": { - "version": "cb8220ef6e1c6dcb45904a596b5df5f905674a71a7032a3e986499c522bdf8cd", + "version": "b769654f69bd018154204952ed7b46a249c225eadfe6b80fd45902e31c28a7c2", "signature": "3c6be598c4405724e8ed2c5bd640c27341b23c15c654ad8943a37100271ede6f" }, "./src/persistutils.ts": { @@ -421,6 +421,10 @@ "version": "b48640c615edeac46a37fa4c3f9427539c7b0c9c9dced07d374dcec44fc4ffb7", "signature": "b48640c615edeac46a37fa4c3f9427539c7b0c9c9dced07d374dcec44fc4ffb7" }, + "./node_modules/colors/index.d.ts": { + "version": "a17e831d16c27cbe4d6f0bb238c1ded0d4a26c282009e431c68733be0b792f4f", + "signature": "a17e831d16c27cbe4d6f0bb238c1ded0d4a26c282009e431c68733be0b792f4f" + }, "./node_modules/@oclif/errors/lib/handle.d.ts": { "version": "207218b7704a71e718a25a35101afb2206581fd07c878c34540038639ad2759e", "signature": "207218b7704a71e718a25a35101afb2206581fd07c878c34540038639ad2759e" @@ -505,14 +509,6 @@ "version": "72b62584101abbf031fd6e73a21842a99877186d422cb9e65aa470c2b98463f4", "signature": "60b33bcb9810f8e80f30a186b41f25d05b97a7781cf8f03edb9315d17a7aeaf1" }, - "./src/commands/generate.ts": { - "version": "017f9750f51931f278a1fe68425c674d64372bec942b3d3e30cdef285ca00a71", - "signature": "f6be6a1f6937bf42d3b2190e690070d2721913b5f162a1a359c189175d932696" - }, - "./node_modules/colors/index.d.ts": { - "version": "a17e831d16c27cbe4d6f0bb238c1ded0d4a26c282009e431c68733be0b792f4f", - "signature": "a17e831d16c27cbe4d6f0bb238c1ded0d4a26c282009e431c68733be0b792f4f" - }, "./node_modules/open/index.d.ts": { "version": "1d5adf522de176e09dcabab58740bce14bb99356fb6474f621a152461a5e4703", "signature": "1d5adf522de176e09dcabab58740bce14bb99356fb6474f621a152461a5e4703" @@ -1161,8 +1157,12 @@ "version": "0b0481c285c517664694fd9f2e7636dfcae7cafff4a54318d2c9453e61ed91b9", "signature": "02d804c0bb60cd4693267f760804c914bd4145db7c176ae1a70f346f0fa104fc" }, + "./src/commands/generate/oas.ts": { + "version": "3f41d1d430eda1bcc52d60ce882522728679aacf461cc45fc53fdf52ac2275f7", + "signature": "7bbc3a0caebfd245e2b09fda2038ed29e8f8082081c2203ffe23b1f068eb0b10" + }, "./src/commands/publish-oas.ts": { - "version": "f334103858999564b3d2cf5c64855f970401acfb14a93fef2e78caf4e6060e92", + "version": "c6342852cf3fb843ccabb1429845f0676bf7f4d6c1ec9472149038cbbb772976", "signature": "a5f91a172a7df9fb875aa8a83eee10175de0ff3ad2566ce4689b9ac27907615e" }, "./node_modules/@types/anymatch/index.d.ts": { @@ -1903,6 +1903,10 @@ "./node_modules/@types/node/util.d.ts", "./node_modules/@types/node/ts3.2/util.d.ts" ], + "./node_modules/colors/index.d.ts": [ + "./node_modules/@types/node/util.d.ts", + "./node_modules/@types/node/ts3.2/util.d.ts" + ], "./node_modules/@oclif/errors/lib/handle.d.ts": [ "./node_modules/@types/node/util.d.ts", "./node_modules/@types/node/ts3.2/util.d.ts" @@ -2010,23 +2014,6 @@ "./node_modules/@types/node/util.d.ts", "./node_modules/@types/node/ts3.2/util.d.ts" ], - "./src/commands/generate.ts": [ - "./node_modules/@oclif/command/lib/index.d.ts", - "./node_modules/@types/fs-extra/index.d.ts", - "./node_modules/clipboardy/index.d.ts", - "./node_modules/@types/node/path.d.ts", - "./node_modules/cli-ux/lib/index.d.ts", - "./src/paths.ts", - "./src/persistutils.ts", - "./node_modules/@types/js-yaml/index.d.ts", - "./src/lib/analytics.ts", - "./node_modules/@types/node/util.d.ts", - "./node_modules/@types/node/ts3.2/util.d.ts" - ], - "./node_modules/colors/index.d.ts": [ - "./node_modules/@types/node/util.d.ts", - "./node_modules/@types/node/ts3.2/util.d.ts" - ], "./node_modules/open/index.d.ts": [ "./node_modules/@types/node/child_process.d.ts", "./node_modules/@types/node/ts3.2/index.d.ts", @@ -3335,6 +3322,19 @@ "./node_modules/@types/node/util.d.ts", "./node_modules/@types/node/ts3.2/util.d.ts" ], + "./src/commands/generate/oas.ts": [ + "./node_modules/@oclif/command/lib/index.d.ts", + "./node_modules/@types/fs-extra/index.d.ts", + "./node_modules/clipboardy/index.d.ts", + "./node_modules/@types/node/path.d.ts", + "./node_modules/cli-ux/lib/index.d.ts", + "./src/paths.ts", + "./src/persistutils.ts", + "./src/lib/analytics.ts", + "./node_modules/@types/js-yaml/index.d.ts", + "./node_modules/@types/node/util.d.ts", + "./node_modules/@types/node/ts3.2/util.d.ts" + ], "./src/commands/publish-oas.ts": [ "./node_modules/@oclif/command/lib/index.d.ts", "./node_modules/@types/fs-extra/index.d.ts", @@ -3343,7 +3343,7 @@ "./node_modules/cli-ux/lib/index.d.ts", "./src/lib/log-helper.ts", "./node_modules/colors/index.d.ts", - "./src/commands/generate.ts", + "./src/commands/generate/oas.ts", "./node_modules/@types/node/child_process.d.ts", "./src/commands/init.ts", "./src/commands/start.ts", @@ -3720,6 +3720,13 @@ "./node_modules/@types/node/util.d.ts", "./node_modules/@types/node/ts3.2/util.d.ts" ], + "./src/commands/generate/oas.ts": [ + "./node_modules/@oclif/parser/lib/flags.d.ts", + "./node_modules/@oclif/command/lib/index.d.ts" + ], + "./src/commands/publish-oas.ts": [ + "./node_modules/@oclif/command/lib/index.d.ts" + ], "./src/commands/intercept.ts": [ "./node_modules/@oclif/parser/lib/flags.d.ts", "./src/lib/proxy-capture-session.ts", @@ -3742,16 +3749,10 @@ "./src/lib/proxy-capture-session.ts", "./node_modules/@oclif/command/lib/index.d.ts" ], - "./src/commands/publish-oas.ts": [ - "./node_modules/@oclif/command/lib/index.d.ts" - ], "./src/commands/init.ts": [ "./node_modules/@oclif/parser/lib/flags.d.ts", "./node_modules/@oclif/command/lib/index.d.ts" ], - "./src/commands/generate.ts": [ - "./node_modules/@oclif/command/lib/index.d.ts" - ], "./node_modules/@types/node/globals.d.ts": [ "./node_modules/@types/node/util.d.ts", "./node_modules/@types/node/ts3.2/util.d.ts" @@ -5439,10 +5440,6 @@ "./node_modules/@types/node/util.d.ts", "./node_modules/@types/node/ts3.2/util.d.ts" ], - "./node_modules/colors/index.d.ts": [ - "./node_modules/@types/node/util.d.ts", - "./node_modules/@types/node/ts3.2/util.d.ts" - ], "./node_modules/keytar/keytar.d.ts": [ "./node_modules/@types/node/util.d.ts", "./node_modules/@types/node/ts3.2/util.d.ts" @@ -5539,6 +5536,10 @@ "./node_modules/@types/node/util.d.ts", "./node_modules/@types/node/ts3.2/util.d.ts" ], + "./node_modules/colors/index.d.ts": [ + "./node_modules/@types/node/util.d.ts", + "./node_modules/@types/node/ts3.2/util.d.ts" + ], "./node_modules/clipboardy/index.d.ts": [ "./node_modules/@types/node/util.d.ts", "./node_modules/@types/node/ts3.2/util.d.ts" @@ -5757,6 +5758,7 @@ "./src/index.ts", "./src/typescript-overrides.d.ts", "./node_modules/clipboardy/index.d.ts", + "./node_modules/colors/index.d.ts", "./node_modules/@oclif/errors/lib/handle.d.ts", "./node_modules/@oclif/errors/lib/errors/cli.d.ts", "./node_modules/@oclif/errors/lib/errors/exit.d.ts", @@ -5778,8 +5780,6 @@ "./node_modules/keytar/keytar.d.ts", "./src/lib/credentials.ts", "./src/lib/analytics.ts", - "./src/commands/generate.ts", - "./node_modules/colors/index.d.ts", "./node_modules/open/index.d.ts", "./src/commands/init.ts", "./node_modules/@types/range-parser/index.d.ts", @@ -5942,6 +5942,7 @@ "./node_modules/mockttp/dist/main.d.ts", "./node_modules/@types/tmp/index.d.ts", "./src/commands/intercept.ts", + "./src/commands/generate/oas.ts", "./src/commands/publish-oas.ts", "./node_modules/@types/anymatch/index.d.ts", "./node_modules/@types/archy/index.d.ts", diff --git a/domain/src/main/scala/com/seamless/contexts/rfc/projections/OASProjection.scala b/domain/src/main/scala/com/seamless/contexts/rfc/projections/OASProjection.scala new file mode 100644 index 0000000000..396c7177a1 --- /dev/null +++ b/domain/src/main/scala/com/seamless/contexts/rfc/projections/OASProjection.scala @@ -0,0 +1,204 @@ +package com.seamless.contexts.rfc.projections + +import com.seamless.contexts.requests.Commands.{BodyDescriptor, ParameterizedPathComponentDescriptor, ShapedBodyDescriptor} +import com.seamless.contexts.requests.{HttpRequest, HttpResponse} +import com.seamless.contexts.rfc.Events.RfcEvent +import com.seamless.contexts.rfc.projections.OASDomain.{Body, Operation, Path, PathParameter, Response} +import com.seamless.contexts.rfc.{InMemoryQueries, RfcService, RfcServiceJSFacade, RfcState} +import com.seamless.contexts.shapes.ShapesHelper +import com.seamless.contexts.shapes.projections.JsonSchemaProjection +import com.seamless.ddd.{AggregateId, InMemoryEventStore} +import io.circe.Json +import io.circe.scalajs.{convertJsToJson, convertJsonToJs} +import scala.scalajs.js.annotation.{JSExport, JSExportAll} +import scala.scalajs.js + +@JSExport +@JSExportAll +object OASProjectionHelper { + def fromEventString(eventString: String): js.Any = { + val eventStore = RfcServiceJSFacade.makeEventStore() + eventStore.bulkAdd("id", eventString) + val rfcService: RfcService = new RfcService(eventStore) + val queries = new InMemoryQueries(eventStore, rfcService, "id") + convertJsonToJs(new OASProjection(queries, rfcService, "id").generate) + } +} + +class OASProjection(queries: InMemoryQueries, rfcService: RfcService, aggregateId: AggregateId) { + + lazy val rfcState = rfcService.currentState(aggregateId) + + lazy val sharedSchemaComponents = rfcState.shapesState.shapes.filter { + case (k, shape) => !shape.isRemoved && shape.descriptor.name != "" && !ShapesHelper.allCoreShapes.exists(_.baseShapeId == k) + } + lazy val pathMapping: Vector[FullPath] = PathListProjection.fromEvents(rfcService.listEvents("id")) + + def bodyToOAS(bodyDescriptor: BodyDescriptor) = { + bodyDescriptor match { + case body: ShapedBodyDescriptor => Some(Body(body.httpContentType, Some(new JsonSchemaProjection(body.shapeId)(rfcState.shapesState).asJsonSchema(expand = false)))) + case _ => None + } + } + + def responsesForRequest(requestId: String): Vector[HttpResponse] = { + queries.requestsState.responses.collect { + case (responseId, response) if response.responseDescriptor.requestId == requestId && !response.isRemoved => response + }.toVector.sortBy(_.responseDescriptor.httpStatusCode) + } + + + def getContributionOption(id: String, key: String) = queries.contributions.get(id, key) + + def operationFromRequest(request: HttpRequest): Operation = { + + val requestBody = bodyToOAS(request.requestDescriptor.bodyDescriptor) +// + val operationId = getContributionOption(request.requestId, "oas.operationId").getOrElse(request.requestId) + val summary = getContributionOption(request.requestId, "oas.operationId") + val description = getContributionOption(request.requestId, "oas.operationId") + + val r = responsesForRequest(request.requestId) + val responses = responsesForRequest(request.requestId).sortBy(_.responseDescriptor.httpStatusCode).map { + case res => { + val responseDescription = getContributionOption(res.responseId, "description") + (res.responseDescriptor.httpStatusCode.toString, Response(responseDescription, bodyToOAS(res.responseDescriptor.bodyDescriptor))) + } + } + + Operation(operationId, summary, description, requestBody, responses) + } + + lazy val oasOperations = { + + def pathParametersForLeaf(id: String) = { + val searchPaths = pathMapping.find(_.pathId == id).get._parentPathIds :+ id + searchPaths.map(i => { + queries.requestsState.pathComponents(i).descriptor + }).collect { + //hardcoding string for now + case param: ParameterizedPathComponentDescriptor => PathParameter(param.name, Json.obj("type" -> Json.fromString("string"))) + } + } + + def requestForId(id: String) = { + queries.requestsState.requests(id) + } + + val pathIdsWithRequests = queries.pathsWithRequests.values.toSet + pathIdsWithRequests.map(pathId => Path( pathMapping.find(_.pathId == pathId).get.absolutePath , { + queries.pathsWithRequests.collect { case i if i._2 == pathId && !requestForId(i._1).isRemoved => + val request = requestForId(i._1) + request.requestDescriptor.httpMethod.toLowerCase -> operationFromRequest(request) + }.toVector.sortBy(_._1).toMap + }, pathParametersForLeaf(pathId))) + } + + + def generate: Json = { + + + val sharedDefinitions = sharedSchemaComponents.map(i => { + import com.seamless.contexts.shapes.projections.JsonSchemaHelpers._ + val name = i._2.descriptor.name + name -> new JsonSchemaProjection(i._1)(rfcState.shapesState).asJsonSchema(expand = true) + }).toSeq + + Json.obj( + "openapi" -> Json.fromString("3.0.1"), + "info" -> Json.obj( + "title" -> Json.fromString(queries.apiName()), + "version" -> Json.fromString(rfcService.listEvents("id").length.toString) + ), + "paths" -> Json.obj( + oasOperations.toVector.sortBy(_.absolutePath).map(path => { + path.absolutePath -> Json.obj( + (path.operations.toVector.sortBy(_._1).map { case (k, v) => k -> v.toJson } :+ path.pathParametersToJson ):_* + ) + }):_* + ), + "components" -> Json.obj( + "schemas" -> Json.obj( + sharedDefinitions:_* + ) + ) + ) + + } + +} + +object OASDomain { + + case class Path(absolutePath: String, operations: Map[String, Operation], pathParameters: Vector[PathParameter]) { + def pathParametersToJson = { + "parameters" -> Json.arr( pathParameters.sortBy(p => absolutePath.indexOf("{"+p+"}")).map(_.toJson):_*) + } + } + case class Operation(operationId: String, + summary: Option[String], + description: Option[String], + requestBody: Option[Body], + responses: Vector[(String, Response)]) + { + + def toJson = { + var json = Json.obj().asObject.get + json = json.add("operationId", Json.fromString(operationId)) + + if (summary.isDefined && summary.get.nonEmpty) { + json = json.add("purpose", Json.fromString(summary.get)) + } + if (description.isDefined && description.get.nonEmpty) { + json = json.add("description", Json.fromString(description.get)) + } + + if (requestBody.isDefined) { + json = json.add("requestBody", requestBody.get.toJson) + } + + if (responses.nonEmpty) { + json = json.add("responses", Json.obj( responses.map(i => (i._1, i._2.toJson)):_* )) + } + + Json.fromJsonObject(json) + } + + } + + case class Body(contentType: String, asJsonSchema: Option[Json]) { + def toJson = { + Json.obj( + "content" -> Json.obj( + contentType -> Json.obj( + "schema" -> asJsonSchema.get + ) + ) + ) + } + } + + case class PathParameter(name: String, asJsonSchema: Json) { + def toJson = { + Json.obj( + "in" -> Json.fromString("path"), + "name" -> Json.fromString(name), + "required" -> Json.fromBoolean(true), + "schema" -> asJsonSchema + ) + } + } + + case class Response(description: Option[String], responseBody: Option[Body]) { + def toJson = { + val base = if (responseBody.isDefined) { + responseBody.get.toJson + } else { + Json.obj() + } + Json.fromJsonObject(base.asObject.get.add("description", Json.fromString(description.getOrElse("")))) + } + } + + +} diff --git a/domain/src/main/scala/com/seamless/contexts/rfc/projections/PathProjection.scala b/domain/src/main/scala/com/seamless/contexts/rfc/projections/PathProjection.scala new file mode 100644 index 0000000000..6fbd18bb75 --- /dev/null +++ b/domain/src/main/scala/com/seamless/contexts/rfc/projections/PathProjection.scala @@ -0,0 +1,57 @@ +package com.seamless.contexts.rfc.projections + +import com.seamless.contexts.requests.Commands.PathComponentId +import com.seamless.contexts.requests.Events.{PathComponentAdded, PathComponentRemoved, PathComponentRenamed, PathParameterAdded, PathParameterRemoved} +import com.seamless.contexts.rfc.Events.RfcEvent +import com.seamless.ddd.Projection + +case class FullPath(pathId: PathComponentId, isParameter: Boolean, _parentPathIds: Vector[PathComponentId], name: String, absolutePath: String, normalizedAbsolutePath: String) + +object PathListProjection extends Projection[RfcEvent, Vector[FullPath]] { + + override def fromEvents(events: Vector[RfcEvent]): Vector[FullPath] = { + val pathMap: Map[PathComponentId, FullPath] = Map("root" -> FullPath("root", isParameter = false,Vector.empty, "", "/", "/")) + withMap(pathMap, events) + } + + override def withInitialState(initialState: Vector[FullPath], events: Vector[RfcEvent]): Vector[FullPath] = { + val pathMap = initialState.map(item => item.pathId -> item).toMap + withMap(pathMap, events) + } + + def joinPath(parentPath: String, pathComponent: String) = { + if (parentPath == "/") parentPath + pathComponent else parentPath + "/" + pathComponent + } + + def withMap(pathMap: Map[PathComponentId, FullPath], events: Vector[RfcEvent]): Vector[FullPath] = { + val results = events.foldLeft(pathMap)((acc, event) => { + event match { + case e: PathComponentAdded => { + val parent = acc(e.parentPathId) + acc + (e.pathId -> FullPath(e.pathId, isParameter = false, e.parentPathId +: parent._parentPathIds, e.name, joinPath(parent.absolutePath, e.name), joinPath(parent.normalizedAbsolutePath, e.name))) + } + case e: PathComponentRenamed => { + val p = acc(e.pathId) + acc.updated(e.pathId, p.copy(name = e.name)) + } + + case e: PathComponentRemoved => { + acc - e.pathId + } + + case e: PathParameterAdded => { + val parent = acc(e.parentPathId) + acc + (e.pathId -> FullPath(e.pathId, isParameter = true, e.parentPathId +: parent._parentPathIds, e.name, joinPath(parent.absolutePath, "{" + e.name + "}"), joinPath(parent.normalizedAbsolutePath, "{" + e.name + "}"))) + } + + case e: PathParameterRemoved => { + acc - e.pathId + } + + case _ => acc + } + }) + + results.values.toVector + } +} diff --git a/domain/src/main/scala/com/seamless/contexts/shapes/ShapesAggregate.scala b/domain/src/main/scala/com/seamless/contexts/shapes/ShapesAggregate.scala index 041a00f1ad..ed44f501f7 100644 --- a/domain/src/main/scala/com/seamless/contexts/shapes/ShapesAggregate.scala +++ b/domain/src/main/scala/com/seamless/contexts/shapes/ShapesAggregate.scala @@ -4,6 +4,7 @@ import com.seamless.contexts.base.BaseCommandContext import com.seamless.contexts.rfc.Events.{EventContext, fromCommandContext} import com.seamless.contexts.shapes.Commands._ import com.seamless.contexts.shapes.Events._ +import com.seamless.contexts.shapes.ShapesHelper.OptionalKind import com.seamless.ddd.{Effects, EventSourcedAggregate} import scala.collection.immutable.ListMap @@ -254,7 +255,7 @@ object ShapesAggregate extends EventSourcedAggregate[ShapesState, ShapesCommand, val oneOfShape = CoreShape(oneOfShapeId, DynamicParameterList(Seq.empty), "OneOf") val optionalShapeId = "$optional" - val optionalParameter = ShapeParameterEntity("$optionalInner", ShapeParameterValue(optionalShapeId, ProviderInShape(optionalShapeId, NoProvider(), "$optionalInner"), "T"), isRemoved = false) + val optionalParameter = ShapeParameterEntity(OptionalKind.innerParam, ShapeParameterValue(optionalShapeId, ProviderInShape(optionalShapeId, NoProvider(), OptionalKind.innerParam), "T"), isRemoved = false) val optionalShape = CoreShape(optionalShapeId, StaticParameterList(Seq(optionalParameter.shapeParameterId)), "Optional") val nullableShapeId = "$nullable" diff --git a/domain/src/main/scala/com/seamless/contexts/shapes/ShapesHelper.scala b/domain/src/main/scala/com/seamless/contexts/shapes/ShapesHelper.scala index c3dc84390a..5df6b854f5 100644 --- a/domain/src/main/scala/com/seamless/contexts/shapes/ShapesHelper.scala +++ b/domain/src/main/scala/com/seamless/contexts/shapes/ShapesHelper.scala @@ -40,17 +40,17 @@ object ShapesHelper { sealed class CoreShapeKind(val baseShapeId: ShapeId, val name: String) case object ObjectKind extends CoreShapeKind("$object", "Object") - case object ListKind extends CoreShapeKind("$list", "List") + case object ListKind extends CoreShapeKind("$list", "List") {def innerParam: String = "$listItem"} case object MapKind extends CoreShapeKind("$map", "Map") case object OneOfKind extends CoreShapeKind("$oneOf", "One of") case object AnyKind extends CoreShapeKind("$any", "Any") case object StringKind extends CoreShapeKind("$string", "String") case object NumberKind extends CoreShapeKind("$number", "Number") case object BooleanKind extends CoreShapeKind("$boolean", "Boolean") - case object IdentifierKind extends CoreShapeKind("$identifier", "Identifier") - case object ReferenceKind extends CoreShapeKind("$reference", "Reference") - case object NullableKind extends CoreShapeKind("$nullable", "Nullable") - case object OptionalKind extends CoreShapeKind("$optional", "Optional") + case object IdentifierKind extends CoreShapeKind("$identifier", "Identifier") {def innerParam: String = "$identifierInner"} + case object ReferenceKind extends CoreShapeKind("$reference", "Reference") {def innerParam: String = "$referenceInner"} + case object NullableKind extends CoreShapeKind("$nullable", "Nullable") {def innerParam: String = "$nullableInner"} + case object OptionalKind extends CoreShapeKind("$optional", "Optional") {def innerParam: String = "$optionalInner"} case object UnknownKind extends CoreShapeKind("$unknown", "Unknown") val allCoreShapes = Set(ObjectKind, ListKind, MapKind, OneOfKind, AnyKind, StringKind, NumberKind, BooleanKind, IdentifierKind, ReferenceKind, NullableKind, OptionalKind, UnknownKind) diff --git a/domain/src/main/scala/com/seamless/contexts/shapes/projections/ExampleProjection.scala b/domain/src/main/scala/com/seamless/contexts/shapes/projections/ExampleProjection.scala index c8000705e6..aba1ddcbc2 100644 --- a/domain/src/main/scala/com/seamless/contexts/shapes/projections/ExampleProjection.scala +++ b/domain/src/main/scala/com/seamless/contexts/shapes/projections/ExampleProjection.scala @@ -21,7 +21,7 @@ object ExampleProjection { private def flatPrimitive(kind: CoreShapeKind, value: String): FlatShape = { val nameComponent = ColoredComponent(value, "primitive", None, primitiveId = Some(kind.baseShapeId)) - FlatShape(kind.baseShapeId, Seq(nameComponent), Seq(), kind.baseShapeId, canName = false) + FlatShape(kind.baseShapeId, Seq(nameComponent), Seq(), kind.baseShapeId, canName = false, Map.empty) } private def jsonToFlatRender(json: Json)(implicit path: Seq[String]): FlatShape = { diff --git a/domain/src/main/scala/com/seamless/contexts/shapes/projections/FlatShapeProjection.scala b/domain/src/main/scala/com/seamless/contexts/shapes/projections/FlatShapeProjection.scala index 7f093c7169..1c8645da21 100644 --- a/domain/src/main/scala/com/seamless/contexts/shapes/projections/FlatShapeProjection.scala +++ b/domain/src/main/scala/com/seamless/contexts/shapes/projections/FlatShapeProjection.scala @@ -18,13 +18,13 @@ object FlatShapeProjection { @JSExportAll case class FlatField(fieldName: String, shape: FlatShape, fieldId: FieldId) @JSExportAll - case class FlatShape(baseShapeId: ShapeId, typeName: Seq[ColoredComponent], fields: Seq[FlatField], id: ShapeId, canName: Boolean) { + case class FlatShape(baseShapeId: ShapeId, typeName: Seq[ColoredComponent], fields: Seq[FlatField], id: ShapeId, canName: Boolean, links: Map[String, ShapeId]) { def joinedTypeName = typeName.map(_.name).mkString(" ") } @JSExportAll case class FlatShapeResult(root: FlatShape, parameterMap: Map[String, FlatShape], pathsForAffectedIds: Vector[Seq[String]]) - private val returnAny = (AnyKind.baseShapeId, FlatShape(AnyKind.baseShapeId, Seq(ColoredComponent("Any", "primitive", primitiveId = Some(AnyKind.baseShapeId))), Seq.empty, "$any", false)) + private val returnAny = (AnyKind.baseShapeId, FlatShape(AnyKind.baseShapeId, Seq(ColoredComponent("Any", "primitive", primitiveId = Some(AnyKind.baseShapeId))), Seq.empty, "$any", false, Map.empty)) def forShapeId(shapeId: ShapeId, fieldIdOption: Option[String] = None, affectedIds: Seq[String] = Seq())(implicit shapesState: ShapesState, expandedName: Boolean = true) = { implicit val parametersByShapeId: mutable.Map[String, FlatShape] = scala.collection.mutable.HashMap[String, FlatShape]() @@ -52,15 +52,15 @@ object FlatShapeProjection { .map(i => (i.shapeId, getFlatShape(i.shapeId, trail and InParameter(i.shapeId))(shapesState, None, false, parametersByShapeId, trailLogger))) .getOrElse(returnAny) - def returnWith( typeName: Seq[ColoredComponent], fields: Seq[FlatField] = Seq(), canName: Boolean = false ) = { - FlatShape(shape.baseShapeId, typeName, fields, shapeId, canName) + def returnWith( typeName: Seq[ColoredComponent], fields: Seq[FlatField] = Seq(), canName: Boolean = false, links: Map[String, ShapeId] ) = { + FlatShape(shape.coreShapeId, typeName, fields, shapeId, canName, links) } shape.coreShapeId match { case ListKind.baseShapeId => { val (innerShapeId, flatShape) = resolveInner("$listItem") addLinkToParameter(innerShapeId, flatShape) - returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName)) + returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName), links = Map("$listItem" -> innerShapeId)) } case MapKind.baseShapeId => { val (keyInnerShapeId, keyFlatShape) = resolveInner("$mapKey") @@ -69,7 +69,7 @@ object FlatShapeProjection { val (valueInnerShapeId, valueFlatShape) = resolveInner("$mapValue") addLinkToParameter(valueInnerShapeId, valueFlatShape) - returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName)) + returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName), links = Map("$mapKey" -> keyInnerShapeId, "$mapValue" -> valueInnerShapeId)) } case OneOfKind.baseShapeId => { val inners = shapesState.shapes(shapeId).descriptor.parameters match { @@ -81,28 +81,30 @@ object FlatShapeProjection { } } - returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName)) + val innerLinks = inners.map(resolveInner).map(_._1).zipWithIndex.map { case (innerId, index) => index.toString -> innerId }.toMap + + returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName), links = innerLinks) } case NullableKind.baseShapeId => { val (innerShapeId, flatShape) = resolveInner("$nullableInner") addLinkToParameter(innerShapeId, flatShape) - returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName)) + returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName), links = Map("$nullableInner" -> innerShapeId)) } case OptionalKind.baseShapeId => { - val (innerShapeId, flatShape) = resolveInner("$optionalInner") + val (innerShapeId, flatShape) = resolveInner(OptionalKind.innerParam) addLinkToParameter(innerShapeId, flatShape) - returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName)) + returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName), links = Map(OptionalKind.innerParam -> innerShapeId)) } case ReferenceKind.baseShapeId => { val (innerShapeId, flatShape) = resolveInner("$referenceInner") addLinkToParameter(innerShapeId, flatShape) - returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName)) + returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName), links = Map("$referenceInner" -> innerShapeId)) } case IdentifierKind.baseShapeId => { val (innerShapeId, flatShape) = resolveInner("$identifierInner") addLinkToParameter(innerShapeId, flatShape) - returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName)) + returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName), links = Map("$identifierInner" -> innerShapeId)) } case ObjectKind.baseShapeId => { val baseObject = ShapeDiffer.resolveBaseObject(shapeId)(shapesState) @@ -125,12 +127,12 @@ object FlatShapeProjection { // }) - FlatShape(baseObject.descriptor.baseShapeId, NameForShapeId.getShapeName(baseObject.shapeId), fields, baseObject.shapeId, canName) + FlatShape(baseObject.descriptor.baseShapeId, NameForShapeId.getShapeName(baseObject.shapeId), fields, baseObject.shapeId, canName, Map.empty) } //fallback to primitives case baseShapeId if ShapesHelper.allCoreShapes.exists(_.baseShapeId == baseShapeId) => - returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName)) + returnWith(NameForShapeId.getShapeName(shapeId, expand = expandedName), links = Map.empty) //fallback for unhandled -- should never be reached case _ => returnAny._2 } diff --git a/domain/src/main/scala/com/seamless/contexts/shapes/projections/JsonSchemaProjection.scala b/domain/src/main/scala/com/seamless/contexts/shapes/projections/JsonSchemaProjection.scala new file mode 100644 index 0000000000..17313e75fd --- /dev/null +++ b/domain/src/main/scala/com/seamless/contexts/shapes/projections/JsonSchemaProjection.scala @@ -0,0 +1,134 @@ +package com.seamless.contexts.shapes.projections + +import com.seamless.contexts.shapes.ShapesHelper.{BooleanKind, ListKind, MapKind, NullableKind, NumberKind, ObjectKind, OptionalKind, ReferenceKind, StringKind} +import com.seamless.contexts.shapes.{ShapesHelper, ShapesState} +import com.seamless.contexts.shapes.projections.FlatShapeProjection.{FlatShape, FlatShapeResult} +import io.circe.Json +import JsonSchemaHelpers._ + +class JsonSchemaProjection(shapeId: String)(implicit shapesState: ShapesState) { + + def asJsonSchema(expand: Boolean): Json = { + val flatShape = FlatShapeProjection.forShapeId(shapeId)(shapesState) + flatShapeToJsonSchema(flatShape.root, expand: Boolean)(flatShape) + } + + + private def flatShapeToJsonSchema(shape: FlatShape, expand: Boolean)(implicit projection: FlatShapeResult): Json = { + val schema = new JsonSchema + val isNamed = shapesState.shapes.get(shape.id).exists(_.descriptor.name != "") + + if (isNamed && !expand) { + val name = shapesState.shapes(shape.id).descriptor.name + schema.addRef(("#/components/schemas/"+ name.toCamelCase).asJson) + return schema.asJson + } + + if (isNamed && expand) { + val name = shapesState.shapes(shape.id).descriptor.name + schema.addTitle(name) + } + + shape.baseShapeId match { + case ObjectKind.baseShapeId => { + schema.assignType("object".asJson) + + val required = new scala.collection.mutable.ListBuffer[String]() + val fields = shape.fields.map { case field => + val fieldName = field.fieldName + val fieldShape = flatShapeToJsonSchema(field.shape, expand = false) + if (field.shape.baseShapeId != OptionalKind.baseShapeId) { + required append fieldName + } + fieldName -> fieldShape + } + schema.addRequired(required.toList) + schema.addProperties(fields) + } + + case ListKind.baseShapeId => { + schema.assignType("array".asJson) + val inner = shape.links(ListKind.innerParam) + schema.addItems(new JsonSchemaProjection(inner).asJsonSchema(expand = false)) + } + + case StringKind.baseShapeId => schema.assignType("string".asJson) + case NumberKind.baseShapeId => schema.assignType("number".asJson) + case BooleanKind.baseShapeId => schema.assignType("boolean".asJson) + + case OptionalKind.baseShapeId => { + val inner = shape.links(OptionalKind.innerParam) + return new JsonSchemaProjection(inner).asJsonSchema(expand = false) + } + + case MapKind.baseShapeId | ReferenceKind.baseShapeId | NullableKind.baseShapeId | OptionalKind.baseShapeId => { + println("Core shape not implemented "+ shape.baseShapeId) + } + + case _ => { + println("OAS not implemented for type "+ shape.baseShapeId) + } + } + + schema.asJson + } + +} + +object JsonSchemaHelpers { + + implicit class IdiomaticJsonString(string: String) { + def asJson = Json.fromString(string) + } + + implicit class StringHelpersImpl(s: String) { + def toCamelCase: String = { + val split = s.split(" ") + val tail = split.tail.map { x => x.head.toUpper + x.tail } + split.head + tail.mkString + } + } + + class JsonSchema { + + private var _internal = Json.obj().asObject.get + + def assignType(json: Json) = { + _internal = _internal.add("type", json) + } + + def addTitle(name: String) = { + _internal = _internal.add("title", Json.fromString(name)) + } + + def addProperties(fields: Seq[(String, Json)]) = { + _internal = _internal.add("properties", Json.obj(fields: _*)) + } + def addRequired(required: List[String]) = { + _internal = _internal.add("required", Json.arr(required.map(_.asJson):_*)) + } + + def addItems(json: Json) = { + _internal = _internal.add("items", json) + } + + def addRef(json: Json) = { + _internal = _internal.add("$ref", json) + } + + def allOf(innerSchemas: Seq[Json]) = { + _internal = Json.obj( + "allOf" -> Json.arr(innerSchemas: _*) + ).asObject.get + } + + def description(string: String) = { + _internal = _internal.add("description", Json.fromString(string)) + } + + def replaceWith(json: Json) = _internal = json.asObject.get + + def asJson = Json.fromJsonObject(_internal) + } + +} diff --git a/domain/src/main/scala/com/seamless/contexts/shapes/projections/NameForShapeId.scala b/domain/src/main/scala/com/seamless/contexts/shapes/projections/NameForShapeId.scala index 691d86152d..5883fc2019 100644 --- a/domain/src/main/scala/com/seamless/contexts/shapes/projections/NameForShapeId.scala +++ b/domain/src/main/scala/com/seamless/contexts/shapes/projections/NameForShapeId.scala @@ -84,7 +84,7 @@ object NameForShapeId { nullableInner ++ Seq(ColoredComponent("(nullable)", "modifier", primitiveId= Some(NullableKind.baseShapeId))) } case OptionalKind.baseShapeId => { - val (innerId, optionalInner) = resolveInner("$optionalInner") + val (innerId, optionalInner) = resolveInner(OptionalKind.innerParam) optionalInner ++ Seq(ColoredComponent("(optional)", "modifier", primitiveId= Some(OptionalKind.baseShapeId))) } case ReferenceKind.baseShapeId => { diff --git a/domain/src/main/scala/com/seamless/diff/ShapeDiffer.scala b/domain/src/main/scala/com/seamless/diff/ShapeDiffer.scala index 4ade74247a..c8e1aa9022 100644 --- a/domain/src/main/scala/com/seamless/diff/ShapeDiffer.scala +++ b/domain/src/main/scala/com/seamless/diff/ShapeDiffer.scala @@ -182,7 +182,7 @@ object ShapeDiffer { } } case OptionalKind => { - val referencedShape = resolveParameterShape(expectedShape.shapeId, "$optionalInner") + val referencedShape = resolveParameterShape(expectedShape.shapeId, OptionalKind.innerParam) if (referencedShape.isDefined) { ShapeDiffer.diff(referencedShape.get, actualShapeOption) } else { diff --git a/domain/src/main/scala/com/seamless/diff/interpreters/OptionalInterpreter.scala b/domain/src/main/scala/com/seamless/diff/interpreters/OptionalInterpreter.scala index 7dde80e966..d7a9c68017 100644 --- a/domain/src/main/scala/com/seamless/diff/interpreters/OptionalInterpreter.scala +++ b/domain/src/main/scala/com/seamless/diff/interpreters/OptionalInterpreter.scala @@ -63,7 +63,7 @@ class OptionalInterpreter(shapesState: ShapesState) extends Interpreter[RequestD case fs: FieldShapeFromShape => ShapeProvider(fs.shapeId) case fs: FieldShapeFromParameter => ParameterProvider(fs.shapeParameterId) }, - "$optionalInner" + OptionalKind.innerParam ) ), SetFieldShape(FieldShapeFromShape(field.fieldId, wrapperShapeId)), @@ -83,7 +83,7 @@ class OptionalInterpreter(shapesState: ShapesState) extends Interpreter[RequestD val commands = Seq( AddShape(wrapperShapeId, OptionalKind.baseShapeId, ""), SetParameterShape( - ProviderInShape(wrapperShapeId, ShapeProvider(shapeDiff.expected.shapeId), "$optionalInner") + ProviderInShape(wrapperShapeId, ShapeProvider(shapeDiff.expected.shapeId), OptionalKind.innerParam) ), SetRequestBodyShape(requestDiffResult.requestId, ShapedBodyDescriptor(requestDiffResult.contentType, wrapperShapeId, isRemoved = false)) ) @@ -102,7 +102,7 @@ class OptionalInterpreter(shapesState: ShapesState) extends Interpreter[RequestD val commands = Seq( AddShape(wrapperShapeId, OptionalKind.baseShapeId, ""), SetParameterShape( - ProviderInShape(wrapperShapeId, ShapeProvider(shapeDiff.expected.shapeId), "$optionalInner") + ProviderInShape(wrapperShapeId, ShapeProvider(shapeDiff.expected.shapeId), OptionalKind.innerParam) ), SetResponseBodyShape(requestDiffResult.responseId, ShapedBodyDescriptor(requestDiffResult.contentType, wrapperShapeId, isRemoved = false)) ) diff --git a/domain/src/test/resources/diff-scenarios/todo-dry.events.json b/domain/src/test/resources/diff-scenarios/todo-dry.events.json new file mode 100644 index 0000000000..d3502ef864 --- /dev/null +++ b/domain/src/test/resources/diff-scenarios/todo-dry.events.json @@ -0,0 +1,53 @@ +[ + {"GitStateSet":{"branchName":"master","commitId":"843a3d0cf8d3db28303970fc9d031edef5e4c2d8","eventContext":{"clientCommandBatchId":"758951cf-6e5f-42c9-8975-5aafc9af3609","clientId":"acunniffe@gmail.com","clientSessionId":"3a9d1781-7e2a-430c-a2ad-e89b731b2987","createdAt":"2019-12-06T19:59:12.261Z"}}} +,{"PathComponentAdded":{"eventContext":{"clientCommandBatchId":"6a48e6e3-e428-4d58-8aba-1bb1b27c9d7c","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:16.599Z"},"name":"todos","parentPathId":"root","pathId":"path_ubDE6MiMpT"}} +,{"RequestAdded":{"eventContext":{"clientCommandBatchId":"82ab54ea-c3c8-4152-8033-2a343672b0a7","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:19.522Z"},"httpMethod":"GET","pathId":"path_ubDE6MiMpT","requestId":"request_CRRIXlDlNB"}} +,{"ContributionAdded":{"eventContext":{"clientCommandBatchId":"82ab54ea-c3c8-4152-8033-2a343672b0a7","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:19.523Z"},"id":"request_CRRIXlDlNB","key":"purpose","value":"Get Todos"}} +,{"ResponseAdded":{"eventContext":{"clientCommandBatchId":"fab68356-dbd7-4f4b-b86b-b9d18ef3e1c3","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:20.374Z"},"httpStatusCode":200,"requestId":"request_CRRIXlDlNB","responseId":"response_axt4cOEjVX"}} +,{"ShapeAdded":{"baseShapeId":"$list","eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.370Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"JDN4GJ_0"}} +,{"ShapeAdded":{"baseShapeId":"$object","eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.370Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"JDN4GJ_1"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.370Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"JDN4GJ_3"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.370Z"},"fieldId":"JDN4GJ_2","name":"dueDate","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"JDN4GJ_2","shapeId":"JDN4GJ_3"}},"shapeId":"JDN4GJ_1"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.370Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"JDN4GJ_5"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.371Z"},"fieldId":"JDN4GJ_4","name":"id","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"JDN4GJ_4","shapeId":"JDN4GJ_5"}},"shapeId":"JDN4GJ_1"}} +,{"ShapeAdded":{"baseShapeId":"$boolean","eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.371Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"JDN4GJ_7"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.371Z"},"fieldId":"JDN4GJ_6","name":"isDone","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"JDN4GJ_6","shapeId":"JDN4GJ_7"}},"shapeId":"JDN4GJ_1"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.371Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"JDN4GJ_9"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.371Z"},"fieldId":"JDN4GJ_8","name":"task","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"JDN4GJ_8","shapeId":"JDN4GJ_9"}},"shapeId":"JDN4GJ_1"}} +,{"ShapeAdded":{"baseShapeId":"JDN4GJ_1","eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.371Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"JDN4GJ_10"}} +,{"ShapeParameterShapeSet":{"eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.371Z"},"shapeDescriptor":{"ProviderInShape":{"consumingParameterId":"$listItem","providerDescriptor":{"ShapeProvider":{"shapeId":"JDN4GJ_10"}},"shapeId":"JDN4GJ_0"}}}} +,{"ShapeAdded":{"baseShapeId":"JDN4GJ_0","eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.372Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_q0gY8gAvcX"}} +,{"ResponseBodySet":{"bodyDescriptor":{"httpContentType":"application/json; charset=utf-8","isRemoved":false,"shapeId":"shape_q0gY8gAvcX"},"eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.372Z"},"responseId":"response_axt4cOEjVX"}} +,{"ShapeRenamed":{"eventContext":{"clientCommandBatchId":"5c1c454a-d520-4a1e-8df9-b239eb0c5dfd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:25.372Z"},"name":"ToDo","shapeId":"JDN4GJ_1"}} +,{"ShapeAdded":{"baseShapeId":"$optional","eventContext":{"clientCommandBatchId":"b873edf8-05c2-485d-8538-bfe24c66d6f8","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:27.014Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_jan42wMGUH"}} +,{"ShapeParameterShapeSet":{"eventContext":{"clientCommandBatchId":"b873edf8-05c2-485d-8538-bfe24c66d6f8","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:27.014Z"},"shapeDescriptor":{"ProviderInShape":{"consumingParameterId":"$optionalInner","providerDescriptor":{"ShapeProvider":{"shapeId":"JDN4GJ_3"}},"shapeId":"shape_jan42wMGUH"}}}} +,{"FieldShapeSet":{"eventContext":{"clientCommandBatchId":"b873edf8-05c2-485d-8538-bfe24c66d6f8","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:27.014Z"},"shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"JDN4GJ_2","shapeId":"shape_jan42wMGUH"}}}} +,{"ShapeRenamed":{"eventContext":{"clientCommandBatchId":"b873edf8-05c2-485d-8538-bfe24c66d6f8","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:27.014Z"},"name":"ToDo","shapeId":"JDN4GJ_1"}} +,{"RequestAdded":{"eventContext":{"clientCommandBatchId":"9736a89c-2098-4714-8b0d-d8e79e91bc98","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:34.275Z"},"httpMethod":"POST","pathId":"path_ubDE6MiMpT","requestId":"request_6yVKPpNNau"}} +,{"ContributionAdded":{"eventContext":{"clientCommandBatchId":"9736a89c-2098-4714-8b0d-d8e79e91bc98","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:34.275Z"},"id":"request_6yVKPpNNau","key":"purpose","value":"Post Todos"}} +,{"ShapeAdded":{"baseShapeId":"$object","eventContext":{"clientCommandBatchId":"e87170aa-600d-41e1-8f18-ddc1390784e4","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:35.048Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"oqPnII_0"}} +,{"ShapeAdded":{"baseShapeId":"$boolean","eventContext":{"clientCommandBatchId":"e87170aa-600d-41e1-8f18-ddc1390784e4","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:35.049Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"oqPnII_2"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"e87170aa-600d-41e1-8f18-ddc1390784e4","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:35.049Z"},"fieldId":"oqPnII_1","name":"isDone","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"oqPnII_1","shapeId":"oqPnII_2"}},"shapeId":"oqPnII_0"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"e87170aa-600d-41e1-8f18-ddc1390784e4","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:35.049Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"oqPnII_4"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"e87170aa-600d-41e1-8f18-ddc1390784e4","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:35.049Z"},"fieldId":"oqPnII_3","name":"task","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"oqPnII_3","shapeId":"oqPnII_4"}},"shapeId":"oqPnII_0"}} +,{"ShapeAdded":{"baseShapeId":"oqPnII_0","eventContext":{"clientCommandBatchId":"e87170aa-600d-41e1-8f18-ddc1390784e4","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:35.049Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_rSL5bkmj1f"}} +,{"RequestBodySet":{"bodyDescriptor":{"httpContentType":"application/json","isRemoved":false,"shapeId":"shape_rSL5bkmj1f"},"eventContext":{"clientCommandBatchId":"e87170aa-600d-41e1-8f18-ddc1390784e4","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:35.049Z"},"requestId":"request_6yVKPpNNau"}} +,{"ResponseAdded":{"eventContext":{"clientCommandBatchId":"8a590e69-52d1-4a3e-8acd-ec17fd144f23","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:35.235Z"},"httpStatusCode":200,"requestId":"request_6yVKPpNNau","responseId":"response_mH9VALt4C6"}} +,{"ShapeAdded":{"baseShapeId":"JDN4GJ_1","eventContext":{"clientCommandBatchId":"0607584e-fcfc-42f4-9f91-eaabeeb161cc","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:35.404Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_40NUsNxVqW"}} +,{"ResponseBodySet":{"bodyDescriptor":{"httpContentType":"application/json; charset=utf-8","isRemoved":false,"shapeId":"shape_40NUsNxVqW"},"eventContext":{"clientCommandBatchId":"0607584e-fcfc-42f4-9f91-eaabeeb161cc","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:35.405Z"},"responseId":"response_mH9VALt4C6"}} +,{"PathParameterAdded":{"eventContext":null,"name":"todoId","parentPathId":"path_ubDE6MiMpT","pathId":"path_0muA2gwEhN"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":null,"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_O0VNCw0mwc"}} +,{"PathParameterShapeSet":{"eventContext":null,"pathId":"path_0muA2gwEhN","shapeDescriptor":{"isRemoved":false,"shapeId":"shape_O0VNCw0mwc"}}} +,{"RequestAdded":{"eventContext":{"clientCommandBatchId":"ecec8724-20a6-4cbe-80b0-be9a58fd75cd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:44.309Z"},"httpMethod":"PATCH","pathId":"path_0muA2gwEhN","requestId":"request_uKHqw2t5H7"}} +,{"ContributionAdded":{"eventContext":{"clientCommandBatchId":"ecec8724-20a6-4cbe-80b0-be9a58fd75cd","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:44.310Z"},"id":"request_uKHqw2t5H7","key":"purpose","value":"Update todo by ID"}} +,{"ShapeAdded":{"baseShapeId":"$object","eventContext":{"clientCommandBatchId":"30908746-b440-4658-b18c-cd4c81740bd5","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:44.547Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"a9WGek_0"}} +,{"ShapeAdded":{"baseShapeId":"$boolean","eventContext":{"clientCommandBatchId":"30908746-b440-4658-b18c-cd4c81740bd5","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:44.547Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"a9WGek_2"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"30908746-b440-4658-b18c-cd4c81740bd5","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:44.547Z"},"fieldId":"a9WGek_1","name":"isDone","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"a9WGek_1","shapeId":"a9WGek_2"}},"shapeId":"a9WGek_0"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"30908746-b440-4658-b18c-cd4c81740bd5","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:44.547Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"a9WGek_4"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"30908746-b440-4658-b18c-cd4c81740bd5","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:44.547Z"},"fieldId":"a9WGek_3","name":"task","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"a9WGek_3","shapeId":"a9WGek_4"}},"shapeId":"a9WGek_0"}} +,{"ShapeAdded":{"baseShapeId":"a9WGek_0","eventContext":{"clientCommandBatchId":"30908746-b440-4658-b18c-cd4c81740bd5","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:44.548Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_omkKhi0yhQ"}} +,{"RequestBodySet":{"bodyDescriptor":{"httpContentType":"application/json","isRemoved":false,"shapeId":"shape_omkKhi0yhQ"},"eventContext":{"clientCommandBatchId":"30908746-b440-4658-b18c-cd4c81740bd5","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:44.548Z"},"requestId":"request_uKHqw2t5H7"}} +,{"ResponseAdded":{"eventContext":{"clientCommandBatchId":"2246291e-093d-45bb-a39c-590ad0019dc7","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:45.276Z"},"httpStatusCode":200,"requestId":"request_uKHqw2t5H7","responseId":"response_nd9psWnF0h"}} +,{"ShapeAdded":{"baseShapeId":"JDN4GJ_1","eventContext":{"clientCommandBatchId":"f54aad2d-72a8-4e4d-9608-2c08ecb252ad","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:45.703Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_PGDymHodJu"}} +,{"ResponseBodySet":{"bodyDescriptor":{"httpContentType":"application/json; charset=utf-8","isRemoved":false,"shapeId":"shape_PGDymHodJu"},"eventContext":{"clientCommandBatchId":"f54aad2d-72a8-4e4d-9608-2c08ecb252ad","clientId":"acunniffe@gmail.com","clientSessionId":"0897e549-8c0b-4f96-8bc6-148b01b730d0","createdAt":"2019-12-06T19:59:45.703Z"},"responseId":"response_nd9psWnF0h"}} +] diff --git a/domain/src/test/resources/diff-scenarios/todo.events.json b/domain/src/test/resources/diff-scenarios/todo.events.json new file mode 100644 index 0000000000..6231df11cf --- /dev/null +++ b/domain/src/test/resources/diff-scenarios/todo.events.json @@ -0,0 +1,70 @@ +[ + {"APINamed":{"eventContext":null,"name":"ToDo"}} +,{"GitStateSet":{"branchName":"master","commitId":"843a3d0cf8d3db28303970fc9d031edef5e4c2d8","eventContext":{"clientCommandBatchId":"7553e386-a19b-4348-9b14-a380952f1eeb","clientId":"acunniffe@gmail.com","clientSessionId":"9cacd440-adf6-4c7a-bb42-7759e8ef8d59","createdAt":"2019-12-06T16:45:30.116Z"}}} +,{"PathComponentAdded":{"eventContext":{"clientCommandBatchId":"d7e81fe3-e328-4453-a501-56047239b82f","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:06.192Z"},"name":"todos","parentPathId":"root","pathId":"path_qArblQGcu7"}} +,{"RequestAdded":{"eventContext":{"clientCommandBatchId":"6e632555-87f5-4ba3-9125-2ec7e14690b6","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:09.241Z"},"httpMethod":"GET","pathId":"path_qArblQGcu7","requestId":"request_H4N9eE2ueT"}} +,{"ContributionAdded":{"eventContext":{"clientCommandBatchId":"6e632555-87f5-4ba3-9125-2ec7e14690b6","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:09.241Z"},"id":"request_H4N9eE2ueT","key":"purpose","value":"Get todos"}} +,{"ResponseAdded":{"eventContext":{"clientCommandBatchId":"ad9a7cf9-f1a5-4d37-b1fc-76d56361478e","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:10.268Z"},"httpStatusCode":200,"requestId":"request_H4N9eE2ueT","responseId":"response_arApyb67Wu"}} +,{"ShapeAdded":{"baseShapeId":"$list","eventContext":{"clientCommandBatchId":"dae10e50-a602-4d7b-bdd1-9ddbcc654555","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:11.059Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"jghsd0_0"}} +,{"ShapeAdded":{"baseShapeId":"$object","eventContext":{"clientCommandBatchId":"dae10e50-a602-4d7b-bdd1-9ddbcc654555","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:11.059Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"jghsd0_1"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"dae10e50-a602-4d7b-bdd1-9ddbcc654555","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:11.059Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"jghsd0_3"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"dae10e50-a602-4d7b-bdd1-9ddbcc654555","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:11.059Z"},"fieldId":"jghsd0_2","name":"id","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"jghsd0_2","shapeId":"jghsd0_3"}},"shapeId":"jghsd0_1"}} +,{"ShapeAdded":{"baseShapeId":"$boolean","eventContext":{"clientCommandBatchId":"dae10e50-a602-4d7b-bdd1-9ddbcc654555","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:11.060Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"jghsd0_5"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"dae10e50-a602-4d7b-bdd1-9ddbcc654555","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:11.060Z"},"fieldId":"jghsd0_4","name":"isDone","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"jghsd0_4","shapeId":"jghsd0_5"}},"shapeId":"jghsd0_1"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"dae10e50-a602-4d7b-bdd1-9ddbcc654555","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:11.060Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"jghsd0_7"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"dae10e50-a602-4d7b-bdd1-9ddbcc654555","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:11.060Z"},"fieldId":"jghsd0_6","name":"task","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"jghsd0_6","shapeId":"jghsd0_7"}},"shapeId":"jghsd0_1"}} +,{"ShapeAdded":{"baseShapeId":"jghsd0_1","eventContext":{"clientCommandBatchId":"dae10e50-a602-4d7b-bdd1-9ddbcc654555","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:11.061Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"jghsd0_8"}} +,{"ShapeParameterShapeSet":{"eventContext":{"clientCommandBatchId":"dae10e50-a602-4d7b-bdd1-9ddbcc654555","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:11.061Z"},"shapeDescriptor":{"ProviderInShape":{"consumingParameterId":"$listItem","providerDescriptor":{"ShapeProvider":{"shapeId":"jghsd0_8"}},"shapeId":"jghsd0_0"}}}} +,{"ShapeAdded":{"baseShapeId":"jghsd0_0","eventContext":{"clientCommandBatchId":"dae10e50-a602-4d7b-bdd1-9ddbcc654555","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:11.061Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_jUPd6UEZGs"}} +,{"ResponseBodySet":{"bodyDescriptor":{"httpContentType":"application/json; charset=utf-8","isRemoved":false,"shapeId":"shape_jUPd6UEZGs"},"eventContext":{"clientCommandBatchId":"dae10e50-a602-4d7b-bdd1-9ddbcc654555","clientId":"acunniffe@gmail.com","clientSessionId":"f2707dc8-2186-4cd3-885d-e94464756165","createdAt":"2019-12-06T16:46:11.061Z"},"responseId":"response_arApyb67Wu"}} +,{"GitStateSet":{"branchName":"master","commitId":"843a3d0cf8d3db28303970fc9d031edef5e4c2d8","eventContext":{"clientCommandBatchId":"ad384e5f-2a6e-4b46-94a8-96ed88807417","clientId":"acunniffe@gmail.com","clientSessionId":"52cab7cf-4b88-4880-80b3-6b116b3bdf3c","createdAt":"2019-12-06T16:47:38.587Z"}}} +,{"GitStateSet":{"branchName":"master","commitId":"843a3d0cf8d3db28303970fc9d031edef5e4c2d8","eventContext":{"clientCommandBatchId":"18a80c63-8975-4a03-abe3-ea637f8a5a84","clientId":"acunniffe@gmail.com","clientSessionId":"64558272-7ebc-4e41-a359-3892576a6e9d","createdAt":"2019-12-06T18:56:40.417Z"}}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"bb6300a0-469b-4eb3-a807-04d60f333a35","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:46.041Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"LrvQ41_0"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"bb6300a0-469b-4eb3-a807-04d60f333a35","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:46.041Z"},"fieldId":"field_UEpeaLlcAX","name":"dueDate","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"field_UEpeaLlcAX","shapeId":"LrvQ41_0"}},"shapeId":"jghsd0_1"}} +,{"ShapeAdded":{"baseShapeId":"$optional","eventContext":{"clientCommandBatchId":"064cb55c-b3e8-4c7e-bc59-784180b566c5","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:47.193Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_vs3HCCEI0J"}} +,{"ShapeParameterShapeSet":{"eventContext":{"clientCommandBatchId":"064cb55c-b3e8-4c7e-bc59-784180b566c5","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:47.193Z"},"shapeDescriptor":{"ProviderInShape":{"consumingParameterId":"$optionalInner","providerDescriptor":{"ShapeProvider":{"shapeId":"LrvQ41_0"}},"shapeId":"shape_vs3HCCEI0J"}}}} +,{"FieldShapeSet":{"eventContext":{"clientCommandBatchId":"064cb55c-b3e8-4c7e-bc59-784180b566c5","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:47.193Z"},"shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"field_UEpeaLlcAX","shapeId":"shape_vs3HCCEI0J"}}}} +,{"PathParameterAdded":{"eventContext":null,"name":"todoId","parentPathId":"path_qArblQGcu7","pathId":"path_XQWUohD4L9"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":null,"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_s2vtP47FgY"}} +,{"PathParameterShapeSet":{"eventContext":null,"pathId":"path_XQWUohD4L9","shapeDescriptor":{"isRemoved":false,"shapeId":"shape_s2vtP47FgY"}}} +,{"RequestAdded":{"eventContext":{"clientCommandBatchId":"f8f194a7-a782-43a2-b598-7f160a03e59d","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:57.394Z"},"httpMethod":"PATCH","pathId":"path_XQWUohD4L9","requestId":"request_Pa77UBarCD"}} +,{"ContributionAdded":{"eventContext":{"clientCommandBatchId":"f8f194a7-a782-43a2-b598-7f160a03e59d","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:57.394Z"},"id":"request_Pa77UBarCD","key":"purpose","value":"Update TODO"}} +,{"ShapeAdded":{"baseShapeId":"$object","eventContext":{"clientCommandBatchId":"4ea96148-fc3d-4d61-8264-df1b8055cf0b","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:57.973Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"Eyd543_0"}} +,{"ShapeAdded":{"baseShapeId":"$boolean","eventContext":{"clientCommandBatchId":"4ea96148-fc3d-4d61-8264-df1b8055cf0b","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:57.973Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"Eyd543_2"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"4ea96148-fc3d-4d61-8264-df1b8055cf0b","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:57.973Z"},"fieldId":"Eyd543_1","name":"isDone","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"Eyd543_1","shapeId":"Eyd543_2"}},"shapeId":"Eyd543_0"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"4ea96148-fc3d-4d61-8264-df1b8055cf0b","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:57.973Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"Eyd543_4"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"4ea96148-fc3d-4d61-8264-df1b8055cf0b","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:57.973Z"},"fieldId":"Eyd543_3","name":"task","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"Eyd543_3","shapeId":"Eyd543_4"}},"shapeId":"Eyd543_0"}} +,{"ShapeAdded":{"baseShapeId":"Eyd543_0","eventContext":{"clientCommandBatchId":"4ea96148-fc3d-4d61-8264-df1b8055cf0b","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:57.974Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_HQJGC3zcvx"}} +,{"RequestBodySet":{"bodyDescriptor":{"httpContentType":"application/json","isRemoved":false,"shapeId":"shape_HQJGC3zcvx"},"eventContext":{"clientCommandBatchId":"4ea96148-fc3d-4d61-8264-df1b8055cf0b","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:57.974Z"},"requestId":"request_Pa77UBarCD"}} +,{"ResponseAdded":{"eventContext":{"clientCommandBatchId":"f93dcda3-c324-453f-b66b-34911a018d21","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:58.143Z"},"httpStatusCode":200,"requestId":"request_Pa77UBarCD","responseId":"response_69pufCOO2t"}} +,{"ShapeAdded":{"baseShapeId":"$object","eventContext":{"clientCommandBatchId":"f3ef721c-9c6d-47fd-9366-2a75476c277c","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:58.294Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"VMPlOJ_0"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"f3ef721c-9c6d-47fd-9366-2a75476c277c","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:58.294Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"VMPlOJ_2"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"f3ef721c-9c6d-47fd-9366-2a75476c277c","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:58.294Z"},"fieldId":"VMPlOJ_1","name":"dueDate","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"VMPlOJ_1","shapeId":"VMPlOJ_2"}},"shapeId":"VMPlOJ_0"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"f3ef721c-9c6d-47fd-9366-2a75476c277c","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:58.294Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"VMPlOJ_4"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"f3ef721c-9c6d-47fd-9366-2a75476c277c","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:58.294Z"},"fieldId":"VMPlOJ_3","name":"id","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"VMPlOJ_3","shapeId":"VMPlOJ_4"}},"shapeId":"VMPlOJ_0"}} +,{"ShapeAdded":{"baseShapeId":"$boolean","eventContext":{"clientCommandBatchId":"f3ef721c-9c6d-47fd-9366-2a75476c277c","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:58.294Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"VMPlOJ_6"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"f3ef721c-9c6d-47fd-9366-2a75476c277c","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:58.294Z"},"fieldId":"VMPlOJ_5","name":"isDone","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"VMPlOJ_5","shapeId":"VMPlOJ_6"}},"shapeId":"VMPlOJ_0"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"f3ef721c-9c6d-47fd-9366-2a75476c277c","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:58.294Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"VMPlOJ_8"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"f3ef721c-9c6d-47fd-9366-2a75476c277c","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:58.295Z"},"fieldId":"VMPlOJ_7","name":"task","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"VMPlOJ_7","shapeId":"VMPlOJ_8"}},"shapeId":"VMPlOJ_0"}} +,{"ShapeAdded":{"baseShapeId":"VMPlOJ_0","eventContext":{"clientCommandBatchId":"f3ef721c-9c6d-47fd-9366-2a75476c277c","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:58.295Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_xLSfCBk76V"}} +,{"ResponseBodySet":{"bodyDescriptor":{"httpContentType":"application/json; charset=utf-8","isRemoved":false,"shapeId":"shape_xLSfCBk76V"},"eventContext":{"clientCommandBatchId":"f3ef721c-9c6d-47fd-9366-2a75476c277c","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:56:58.295Z"},"responseId":"response_69pufCOO2t"}} +,{"RequestAdded":{"eventContext":{"clientCommandBatchId":"5b9b2514-db9a-480a-bfc1-abc9c2ffcc17","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:06.248Z"},"httpMethod":"POST","pathId":"path_qArblQGcu7","requestId":"request_jSdvEiRo9a"}} +,{"ContributionAdded":{"eventContext":{"clientCommandBatchId":"5b9b2514-db9a-480a-bfc1-abc9c2ffcc17","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:06.248Z"},"id":"request_jSdvEiRo9a","key":"purpose","value":"Create todo"}} +,{"ShapeAdded":{"baseShapeId":"$object","eventContext":{"clientCommandBatchId":"da83ad62-2a66-4f63-9f2b-4a3fe788c46d","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:06.807Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"Do9MEA_0"}} +,{"ShapeAdded":{"baseShapeId":"$boolean","eventContext":{"clientCommandBatchId":"da83ad62-2a66-4f63-9f2b-4a3fe788c46d","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:06.808Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"Do9MEA_2"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"da83ad62-2a66-4f63-9f2b-4a3fe788c46d","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:06.808Z"},"fieldId":"Do9MEA_1","name":"isDone","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"Do9MEA_1","shapeId":"Do9MEA_2"}},"shapeId":"Do9MEA_0"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"da83ad62-2a66-4f63-9f2b-4a3fe788c46d","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:06.808Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"Do9MEA_4"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"da83ad62-2a66-4f63-9f2b-4a3fe788c46d","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:06.808Z"},"fieldId":"Do9MEA_3","name":"task","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"Do9MEA_3","shapeId":"Do9MEA_4"}},"shapeId":"Do9MEA_0"}} +,{"ShapeAdded":{"baseShapeId":"Do9MEA_0","eventContext":{"clientCommandBatchId":"da83ad62-2a66-4f63-9f2b-4a3fe788c46d","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:06.808Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_R3MxTBL4Hy"}} +,{"RequestBodySet":{"bodyDescriptor":{"httpContentType":"application/json","isRemoved":false,"shapeId":"shape_R3MxTBL4Hy"},"eventContext":{"clientCommandBatchId":"da83ad62-2a66-4f63-9f2b-4a3fe788c46d","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:06.808Z"},"requestId":"request_jSdvEiRo9a"}} +,{"ResponseAdded":{"eventContext":{"clientCommandBatchId":"f02b37bb-f4a6-4aa8-91fb-32bdc0dda5af","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:06.974Z"},"httpStatusCode":200,"requestId":"request_jSdvEiRo9a","responseId":"response_Y6c4kqh5ru"}} +,{"ShapeAdded":{"baseShapeId":"$object","eventContext":{"clientCommandBatchId":"2eb865ad-b1c6-471a-8a1b-48b020124b3f","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:07.142Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"TtrW0r_0"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"2eb865ad-b1c6-471a-8a1b-48b020124b3f","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:07.142Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"TtrW0r_2"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"2eb865ad-b1c6-471a-8a1b-48b020124b3f","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:07.142Z"},"fieldId":"TtrW0r_1","name":"id","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"TtrW0r_1","shapeId":"TtrW0r_2"}},"shapeId":"TtrW0r_0"}} +,{"ShapeAdded":{"baseShapeId":"$boolean","eventContext":{"clientCommandBatchId":"2eb865ad-b1c6-471a-8a1b-48b020124b3f","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:07.142Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"TtrW0r_4"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"2eb865ad-b1c6-471a-8a1b-48b020124b3f","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:07.142Z"},"fieldId":"TtrW0r_3","name":"isDone","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"TtrW0r_3","shapeId":"TtrW0r_4"}},"shapeId":"TtrW0r_0"}} +,{"ShapeAdded":{"baseShapeId":"$string","eventContext":{"clientCommandBatchId":"2eb865ad-b1c6-471a-8a1b-48b020124b3f","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:07.143Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"TtrW0r_6"}} +,{"FieldAdded":{"eventContext":{"clientCommandBatchId":"2eb865ad-b1c6-471a-8a1b-48b020124b3f","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:07.143Z"},"fieldId":"TtrW0r_5","name":"task","shapeDescriptor":{"FieldShapeFromShape":{"fieldId":"TtrW0r_5","shapeId":"TtrW0r_6"}},"shapeId":"TtrW0r_0"}} +,{"ShapeAdded":{"baseShapeId":"TtrW0r_0","eventContext":{"clientCommandBatchId":"2eb865ad-b1c6-471a-8a1b-48b020124b3f","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:07.143Z"},"name":"","parameters":{"DynamicParameterList":{"shapeParameterIds":[]}},"shapeId":"shape_nipWvy4n2j"}} +,{"ResponseBodySet":{"bodyDescriptor":{"httpContentType":"application/json; charset=utf-8","isRemoved":false,"shapeId":"shape_nipWvy4n2j"},"eventContext":{"clientCommandBatchId":"2eb865ad-b1c6-471a-8a1b-48b020124b3f","clientId":"acunniffe@gmail.com","clientSessionId":"661036ff-78db-407e-a94f-581f748f456b","createdAt":"2019-12-06T18:57:07.143Z"},"responseId":"response_Y6c4kqh5ru"}} +] diff --git a/domain/src/test/scala/com/seamless/contexts/rfc/OASProjectionSpec.scala b/domain/src/test/scala/com/seamless/contexts/rfc/OASProjectionSpec.scala new file mode 100644 index 0000000000..72e919716b --- /dev/null +++ b/domain/src/test/scala/com/seamless/contexts/rfc/OASProjectionSpec.scala @@ -0,0 +1,42 @@ +package com.seamless.contexts.rfc + +import com.seamless.contexts.rfc.projections.OASProjection +import com.seamless.contexts.shapes.projections.{JsonSchemaProjection} +import com.seamless.diff.JsonFileFixture +import org.scalatest.FunSpec + +class OASProjectionSpec extends FunSpec with JsonFileFixture { + val commandContext: RfcCommandContext = RfcCommandContext("a", "b", "c") + + def fixture(slug: String): (InMemoryQueries, RfcService, RfcState) = { + + val eventStore = RfcServiceJSFacade.makeEventStore() + eventStore.append("id", eventsFrom(slug)) + val rfcService: RfcService = new RfcService(eventStore) + + (new InMemoryQueries(eventStore, rfcService, "id"), rfcService, rfcService.currentState("id")) + } + + it("works for basic todo app") { + val (queries, rfcService, rfcState) = fixture("todo") + val oas = new OASProjection(queries, rfcService, "id") + assert(oas.generate.noSpaces == "{\"openapi\":\"3.0.1\",\"info\":{\"title\":\"ToDo\",\"version\":\"68\"},\"paths\":{\"/todos\":{\"get\":{\"operationId\":\"request_H4N9eE2ueT\",\"responses\":{\"200\":{\"content\":{\"application/json; charset=utf-8\":{\"schema\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"required\":[\"id\",\"isDone\",\"task\"],\"properties\":{\"dueDate\":{\"type\":\"string\"},\"id\":{\"type\":\"string\"},\"isDone\":{\"type\":\"boolean\"},\"task\":{\"type\":\"string\"}}}}}},\"description\":\"\"}}},\"post\":{\"operationId\":\"request_jSdvEiRo9a\",\"requestBody\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"required\":[\"isDone\",\"task\"],\"properties\":{\"isDone\":{\"type\":\"boolean\"},\"task\":{\"type\":\"string\"}}}}}},\"responses\":{\"200\":{\"content\":{\"application/json; charset=utf-8\":{\"schema\":{\"type\":\"object\",\"required\":[\"id\",\"isDone\",\"task\"],\"properties\":{\"id\":{\"type\":\"string\"},\"isDone\":{\"type\":\"boolean\"},\"task\":{\"type\":\"string\"}}}}},\"description\":\"\"}}},\"parameters\":[]},\"/todos/{todoId}\":{\"patch\":{\"operationId\":\"request_Pa77UBarCD\",\"requestBody\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"required\":[\"isDone\",\"task\"],\"properties\":{\"isDone\":{\"type\":\"boolean\"},\"task\":{\"type\":\"string\"}}}}}},\"responses\":{\"200\":{\"content\":{\"application/json; charset=utf-8\":{\"schema\":{\"type\":\"object\",\"required\":[\"dueDate\",\"id\",\"isDone\",\"task\"],\"properties\":{\"dueDate\":{\"type\":\"string\"},\"id\":{\"type\":\"string\"},\"isDone\":{\"type\":\"boolean\"},\"task\":{\"type\":\"string\"}}}}},\"description\":\"\"}}},\"parameters\":[{\"in\":\"path\",\"name\":\"todoId\",\"required\":true,\"schema\":{\"type\":\"string\"}}]}},\"components\":{\"schemas\":{}}}") + } + + it("works for basic dry todo app") { + val (queries, rfcService, rfcState) = fixture("todo-dry") + val oas = new OASProjection(queries, rfcService, "id") + assert(oas.generate.noSpaces == "{\"openapi\":\"3.0.1\",\"info\":{\"title\":\"Unnamed API\",\"version\":\"51\"},\"paths\":{\"/todos\":{\"get\":{\"operationId\":\"request_CRRIXlDlNB\",\"responses\":{\"200\":{\"content\":{\"application/json; charset=utf-8\":{\"schema\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/components/schemas/ToDo\"}}}},\"description\":\"\"}}},\"post\":{\"operationId\":\"request_6yVKPpNNau\",\"requestBody\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"required\":[\"isDone\",\"task\"],\"properties\":{\"isDone\":{\"type\":\"boolean\"},\"task\":{\"type\":\"string\"}}}}}},\"responses\":{\"200\":{\"content\":{\"application/json; charset=utf-8\":{\"schema\":{\"$ref\":\"#/components/schemas/ToDo\"}}},\"description\":\"\"}}},\"parameters\":[]},\"/todos/{todoId}\":{\"patch\":{\"operationId\":\"request_uKHqw2t5H7\",\"requestBody\":{\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"required\":[\"isDone\",\"task\"],\"properties\":{\"isDone\":{\"type\":\"boolean\"},\"task\":{\"type\":\"string\"}}}}}},\"responses\":{\"200\":{\"content\":{\"application/json; charset=utf-8\":{\"schema\":{\"$ref\":\"#/components/schemas/ToDo\"}}},\"description\":\"\"}}},\"parameters\":[{\"in\":\"path\",\"name\":\"todoId\",\"required\":true,\"schema\":{\"type\":\"string\"}}]}},\"components\":{\"schemas\":{\"ToDo\":{\"type\":\"object\",\"required\":[\"id\",\"isDone\",\"task\"],\"properties\":{\"dueDate\":{\"type\":\"string\"},\"id\":{\"type\":\"string\"},\"isDone\":{\"type\":\"boolean\"},\"task\":{\"type\":\"string\"}}}}}}") + } + + describe("Json Schema") { + + it("works for todo shape") { + val (queries, rfcService, rfcState) = fixture("todo") + val todoSchema = new JsonSchemaProjection("jghsd0_1")(rfcState.shapesState).asJsonSchema(expand = false) + assert(todoSchema.noSpaces == "{\"type\":\"object\",\"required\":[\"id\",\"isDone\",\"task\"],\"properties\":{\"dueDate\":{\"type\":\"string\"},\"id\":{\"type\":\"string\"},\"isDone\":{\"type\":\"boolean\"},\"task\":{\"type\":\"string\"}}}") + } + + } +} + diff --git a/domain/src/test/scala/com/seamless/diff/JsonFileFixture.scala b/domain/src/test/scala/com/seamless/diff/JsonFileFixture.scala index 77ed07cd18..c6831ef056 100644 --- a/domain/src/test/scala/com/seamless/diff/JsonFileFixture.scala +++ b/domain/src/test/scala/com/seamless/diff/JsonFileFixture.scala @@ -4,8 +4,8 @@ import io.circe.Json import io.circe.jawn.parseFile import java.io.File -import com.seamless.contexts.rfc.Commands -import com.seamless.serialization.CommandSerialization +import com.seamless.contexts.rfc.{Commands, Events} +import com.seamless.serialization.{CommandSerialization, EventSerialization} import scala.util.Try @@ -19,4 +19,9 @@ trait JsonFileFixture { val json = parseFile(new File(filePath)).right.get CommandSerialization.fromJson(json).get } -} \ No newline at end of file + def eventsFrom(slug: String): Vector[Events.RfcEvent] = { + val filePath = "src/test/resources/diff-scenarios/" + slug + ".events.json" + val json = parseFile(new File(filePath)).right.get + EventSerialization.fromJson(json).get + } +} diff --git a/domain/src/test/scala/com/seamless/diff/ShapeDifferSpec.scala b/domain/src/test/scala/com/seamless/diff/ShapeDifferSpec.scala index 156ab06e6d..77a47d5454 100644 --- a/domain/src/test/scala/com/seamless/diff/ShapeDifferSpec.scala +++ b/domain/src/test/scala/com/seamless/diff/ShapeDifferSpec.scala @@ -1,6 +1,7 @@ package com.seamless.diff import com.seamless.contexts.shapes.Commands.{DynamicParameterList, NoParameterList, ProviderInShape, ShapeProvider} +import com.seamless.contexts.shapes.ShapesHelper.OptionalKind import com.seamless.contexts.shapes.{ShapeEntity, ShapeValue, ShapesAggregate, ShapesState} import com.seamless.diff.ShapeDiffer.MultipleInterpretations import io.circe.Json @@ -92,7 +93,7 @@ class ShapeDifferSpec extends FunSpec { val expected = ShapeEntity("$x", shape, isRemoved = false) implicit val shapesState: ShapesState = ShapesAggregate.initialState .withShape(expected.shapeId, shape.baseShapeId, shape.parameters, shape.name) - .withShapeParameterShape(ProviderInShape(expected.shapeId, ShapeProvider("$string"), "$optionalInner")) + .withShapeParameterShape(ProviderInShape(expected.shapeId, ShapeProvider("$string"), OptionalKind.innerParam)) describe("when given a string") { it("should return an empty diff") {