Skip to content

Commit

Permalink
feat(mvc) - formalize mvc output rendering
Browse files Browse the repository at this point in the history
progress #24
  • Loading branch information
Daniel Schaffer committed Feb 27, 2019
1 parent 3dee0cd commit 4b793e0
Show file tree
Hide file tree
Showing 27 changed files with 856 additions and 133 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"update-package-configs": "builder update-configs",
"update-packages": "builder npm update",
"build": "builder build",
"test": "mocha $NODE_DEBUG_OPTION",
"test": "mocha",
"lint": "eslint . --ext js,ts",
"lint:fix": "npm run lint -- --fix",
"precoverage": "rimraf coverage .nyc_output",
Expand Down
38 changes: 35 additions & 3 deletions packages/dandi/core-testing/src/test.harness.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Disposable } from '@dandi/common'
import { Container, InjectionToken, Repository, ResolveResult, Resolver, ResolverContext } from '@dandi/core'
import { Disposable, isConstructor } from '@dandi/common'
import { Container, InjectionToken, Repository, ResolveResult, Resolver, ResolverContext, Provider } from '@dandi/core'
import { isFactoryProvider } from '@dandi/core/testing'

import { SinonStub, SinonStubbedInstance, stub } from 'sinon'
import { expect } from 'chai'

import { StubResolverContextFactory } from './stub-resolver-context-factory'

export type TestProvider<T> = Provider<T> & { underTest?: boolean }

export interface TestResolver extends Resolver {
readonly container: Container;

Expand Down Expand Up @@ -49,6 +52,7 @@ export class TestHarness implements TestResolver {

// sanity checking!
expect(repo.get(__TestSanityChecker)).not.to.exist
// eslint-disable-next-line no-invalid-this
repo.register(this, __TestSanityChecker)
expect(repo.get(__TestSanityChecker)).to.exist

Expand All @@ -71,7 +75,22 @@ export class TestHarness implements TestResolver {
}
if (suite) {
beforeEach(async () => {
this._container = new Container({ providers })
const singletonedProviders = providers.map(provider => {
// allow forcing singletons for providers that don't allow singletons
if (!isFactoryProvider(provider) || provider.singleton || (provider as TestProvider<any>).underTest) {
return provider
}
let instance
return Object.assign({}, provider, {
useFactory(...args: any[]) {
if (!instance) {
instance = provider.useFactory(...args)
}
return instance
},
})
})
this._container = new Container({ providers: singletonedProviders })
await this._container.start()
})
afterEach(() => {
Expand Down Expand Up @@ -154,3 +173,16 @@ export async function stubHarnessSingle(...providers: any[]): Promise<TestResolv
await harness.container.start()
return harness
}

export function underTest<T>(provider: Provider<T>): TestProvider<any> {
if (isConstructor(provider)) {
return {
provide: provider,
useClass: provider,
underTest: true,
}
}
return Object.assign({
underTest: true,
}, provider)
}
2 changes: 1 addition & 1 deletion packages/dandi/core/src/injection.token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function getTokenString(token: InjectionToken<any> | Function): string {
if (typeof token === 'function') {
return token.name
}
return (isMappedInjectionToken(token) ? token.provide : token)
return ((isMappedInjectionToken(token) ? token.provide : token) || 'undefined')
.toString()
.replace(/[\W-]+/g, '_')
.replace(/_$/, '')
Expand Down
1 change: 1 addition & 0 deletions packages/dandi/core/testing.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { INJECTABLE_REGISTRATION_SOURCE } from './src/injectable.decorator'
export * from './src/provider.util'
2 changes: 1 addition & 1 deletion packages/dandi/mvc-hal/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export * from './src/accessor.resource.id.decorator'
export * from './src/composition.context'
export * from './src/default.resource.composer'
export * from './src/hal.controller.result'
export * from './src/hal-object-renderer'
export * from './src/hal.result.transformer'
export * from './src/mvc.hal-module'
export * from './src/resource.accessor.decorator'
Expand Down
69 changes: 69 additions & 0 deletions packages/dandi/mvc-hal/src/hal-object-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Inject, Injectable, Optional, Resolver } from '@dandi/core'
import {
ConfiguredObjectRenderer,
ObjectRenderer,
ObjectRendererBase,
ObjectRendererConfig,
parseMimeTypes,
selectRenderer,
} from '@dandi/mvc'

export enum HalMimeTypes {
halJson = 'application/hal+json',
halXml = 'application/hal+xml',
halYaml = 'application/hal+yaml',
}

export interface HalObjectRendererConfig extends ObjectRendererConfig {
outputFormats: string[]
}

@Injectable(ObjectRenderer)
export class HalObjectRenderer extends ObjectRendererBase {

public static accept(types: string): ConfiguredObjectRenderer {
return this.acceptInternal(HalObjectRenderer, types)
}

public readonly defaultContentType: string = HalMimeTypes.halJson

private readonly ready: Promise<void>
private renderers: Map<string, ObjectRenderer>

constructor(
@Inject(Resolver) private resolver: Resolver,
@Inject(ObjectRendererConfig(HalObjectRenderer)) @Optional() config: HalObjectRendererConfig,
) {
super(parseMimeTypes(HalMimeTypes.halJson), config)


this.ready = this.init()
}

private async init(): Promise<void> {
return this.resolver.invoke(this, this.initRenderers)
}

private initRenderers(@Inject(ObjectRenderer) renderers: ObjectRenderer[]) {
const typeMap = this.renderableTypes.map(renderableType => {
const baseType = parseMimeTypes(`${renderableType.type}/${renderableType.subtypeBase}`)
return {
original: renderableType,
baseType,
}
})
this.renderers = typeMap.reduce((result, type) => {
const renderer = selectRenderer(type.baseType, renderers)
result.set(`${type.original.type}/${type.original.subtype}`, renderer)
return result
}, new Map<string, ObjectRenderer>())
}

public async renderObject(contentType: string, value: any): Promise<string> {
await this.ready
const renderer = this.renderers.get(contentType)
const result = await renderer.render(parseMimeTypes(contentType), value)
return result.renderedOutput
}

}
13 changes: 0 additions & 13 deletions packages/dandi/mvc-hal/src/hal.controller.result.ts

This file was deleted.

6 changes: 4 additions & 2 deletions packages/dandi/mvc-hal/src/hal.result.transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Resource, SELF_RELATION } from '@dandi/hal'
import { ControllerResult, ControllerResultTransformer, MvcRequest, ParamMap, RequestQueryParamMap } from '@dandi/mvc'

import { CompositionContext } from './composition.context'
import { HalControllerResult } from './hal.controller.result'
import { ResourceComposer } from './resource.composer'

export const EMBED_RELS_KEY = '_embedded'
Expand Down Expand Up @@ -33,7 +32,10 @@ export class HalResultTransformer implements ControllerResultTransformer {
result.resultObject,
context,
)
return new HalControllerResult(resource, result.headers)
return {
resultObject: resource,
headers: result.headers,
}
})
}
}
3 changes: 2 additions & 1 deletion packages/dandi/mvc-hal/src/mvc.hal-module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ModuleBuilder, Registerable } from '@dandi/core'

import { DefaultResourceComposer } from './default.resource.composer'
import { HalObjectRenderer } from './hal-object-renderer'
import { HalResultTransformer } from './hal.result.transformer'
import { PKG } from './local.token'

Expand All @@ -10,4 +11,4 @@ export class MvcHalModuleBuilder extends ModuleBuilder<MvcHalModuleBuilder> {
}
}

export const MvcHalModule = new MvcHalModuleBuilder(DefaultResourceComposer, HalResultTransformer)
export const MvcHalModule = new MvcHalModuleBuilder(DefaultResourceComposer, HalResultTransformer, HalObjectRenderer)
11 changes: 10 additions & 1 deletion packages/dandi/mvc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,23 @@ export * from './src/errors'
export * from './src/http.method'
export { HttpGet, HttpDelete, HttpOptions, HttpPatch, HttpPost, HttpPut } from './src/http.method.decorator'
export * from './src/http.status.code'
export * from './src/json.controller.result'
export * from './src/mime-type'
export * from './src/mime-type-info'
export * from './src/mime-types'
export * from './src/mvc.module'
export * from './src/mvc.request'
export * from './src/mvc.response'
export * from './src/mvc-response-renderer'
export * from './src/native-json-renderer'
export * from './src/object-renderer'
export * from './src/object-renderer-base'
export * from './src/object-renderer-config'
export * from './src/path.param.decorator'
export * from './src/perf.record'
export * from './src/perf.recorder'
export * from './src/plain-text-renderer'
export * from './src/query.param.decorator'
export * from './src/request-accept-types'
export * from './src/request.authorization.service'
export * from './src/request.body.decorator'
export * from './src/request.info'
Expand Down
8 changes: 3 additions & 5 deletions packages/dandi/mvc/src/controller.result.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
export interface ControllerResult {
readonly resultObject: object;
readonly contentType: string;
readonly headers?: { [key: string]: string };
readonly value: string | Promise<string>;
readonly resultObject: object
readonly headers?: { [key: string]: string }
}

export function isControllerResult(obj: any): obj is ControllerResult {
return obj && typeof obj.value !== 'undefined' && typeof obj.contentType === 'string'
return obj && typeof obj.resultObject !== 'undefined'
}
Loading

0 comments on commit 4b793e0

Please sign in to comment.