Skip to content

Commit

Permalink
feat(mvc) - object renderer view engine integration
Browse files Browse the repository at this point in the history
progress #24
  • Loading branch information
Daniel Schaffer committed Feb 28, 2019
1 parent a82f8b9 commit 4e307da
Show file tree
Hide file tree
Showing 25 changed files with 118 additions and 125 deletions.
2 changes: 1 addition & 1 deletion _examples/simple-express-rest-api/src/server.container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const server = new Container({
LoggingModule.use(ConsoleLogListener),

// MVC
MvcExpressModule.withDefaults().config({ port: parseInt(process.env.PORT, 10) || DEFAULT_SERVER_PORT }),
MvcExpressModule.config({ port: parseInt(process.env.PORT, 10) || DEFAULT_SERVER_PORT }),
MvcViewModule
.engine('pug', PugViewEngine)
.engine('ejs', EjsViewEngine),
Expand Down
57 changes: 11 additions & 46 deletions packages/dandi-contrib/mvc-express/src/mvc-express.module.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,25 @@
import { Constructor } from '@dandi/common'
import { Bootstrapper, ModuleBuilder, Provider, Registerable } from '@dandi/core'
import { Route, RouteExecutor, RouteGenerator, RouteHandler, RouteInitializer, RouteMapper } from '@dandi/mvc'
import { Express } from 'express'
import { ModuleBuilder, Registerable } from '@dandi/core'
import { MvcModule } from '@dandi/mvc'

import { DEFAULT_EXPRESS_PROVIDER } from './default.express.provider'
import { ExpressMvcApplication } from './express.mvc.application'
import { ExpressMvcConfig } from './express.mvc.config'
import { ExpressMvcRouteMapper } from './express.mvc.route.mapper'
import { PKG } from './local.token'

export interface ExpressMvcApplicationConfig {
expressInstanceProvider: Provider<Express>;
routeExecutor: Constructor<RouteExecutor>;
routeGenerator: Constructor<RouteGenerator>;
routeHandler: Constructor<RouteHandler>;
routeInitializer: Constructor<RouteInitializer>;
routeMapper: Constructor<RouteMapper>;
bootstrap: Constructor<Bootstrapper>;
routesProvider: Provider<Route[]>;
}
export type ExpressMvcApplicationOptions = {
[P in keyof ExpressMvcApplicationConfig]?: ExpressMvcApplicationConfig[P]
}

export class MvcExpressModuleBuilder extends ModuleBuilder<MvcExpressModuleBuilder> {
constructor(...entries: Registerable[]) {
super(MvcExpressModuleBuilder, PKG, ...entries)
}

private getConfiguredRegisterables(options: ExpressMvcApplicationOptions) {
const config: ExpressMvcApplicationConfig = {
expressInstanceProvider:
options.expressInstanceProvider || require('./default.express.provider').DEFAULT_EXPRESS_PROVIDER,
routeExecutor: options.routeExecutor || require('@dandi/mvc').DefaultRouteExecutor,
routeGenerator: options.routeGenerator || require('@dandi/mvc').DecoratorRouteGenerator,
routeHandler: options.routeHandler || require('@dandi/mvc').DefaultRouteHandler,
routeInitializer: options.routeInitializer || require('@dandi/mvc').DefaultRouteInitializer,
routeMapper: options.routeMapper || require('./express.mvc.route.mapper').ExpressMvcRouteMapper,
bootstrap: options.bootstrap || require('./express.mvc.application').ExpressMvcApplication,
routesProvider: options.routesProvider || require('@dandi/mvc').ROUTES_PROVIDER,
}
return Object.values(config).filter((v) => v)
}

public providers(options: ExpressMvcApplicationOptions = {}): this {
return this.add(...this.getConfiguredRegisterables(options))
}

public config(mvcConfig: ExpressMvcConfig): this {
return this.add({ provide: ExpressMvcConfig, useValue: mvcConfig })
}
}

export class MvcExpressModule {
public static withProviders(options: ExpressMvcApplicationOptions): MvcExpressModuleBuilder {
return new MvcExpressModuleBuilder().providers(options)
}

public static withDefaults(): MvcExpressModuleBuilder {
return new MvcExpressModuleBuilder().providers()
}
}
export const MvcExpressModule = new MvcExpressModuleBuilder(
MvcModule,
DEFAULT_EXPRESS_PROVIDER,
ExpressMvcApplication,
ExpressMvcRouteMapper,
)
2 changes: 1 addition & 1 deletion packages/dandi/core/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Provider } from './provider'
import { isProvider } from './provider.util'
import { InjectionToken, isInjectionToken } from './injection.token'

export type RegisterableTypes = Constructor<any> | Provider<any> | Array<Constructor<any>> | Array<Provider<any>>
export type RegisterableTypes = Constructor<any> | Provider<any> | Array<Constructor<any>> | Array<Provider<any>> | Module
export type Registerable = RegisterableTypes | RegisterableTypes[]

export interface ModuleInfo {
Expand Down
2 changes: 1 addition & 1 deletion packages/dandi/mvc-hal/src/default.resource.composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ export class DefaultResourceComposer implements ResourceComposer {
@Inject(CompositionContext) compositionContext: CompositionContext,
): Promise<ComposedResource<any> | ComposedResource<any>[]> {
const result = await this.resolver.invokeInContext(resolverContext, controller, controller[route.controllerMethod])
const resultResource: any = isControllerResult(result) ? result.resultObject : result
const resultResource: any = isControllerResult(result) ? result.data : result
if (Array.isArray(resultResource)) {
return Promise.all(
resultResource.map((resource) =>
Expand Down
6 changes: 3 additions & 3 deletions packages/dandi/mvc-hal/src/hal-object-renderer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('HalObjectRenderer', function() {

const value = { foo: 'bar' }

await this.renderer.renderObject(HalMimeTypes.halJson, value)
await this.renderer.renderControllerResult(HalMimeTypes.halJson, value)

expect(jsonRenderer.render).to.have.been
.calledOnce
Expand All @@ -59,14 +59,14 @@ describe('HalObjectRenderer', function() {

const value = { foo: 'bar' }

const result = await this.renderer.renderObject(HalMimeTypes.halJson, value)
const result = await this.renderer.renderControllerResult(HalMimeTypes.halJson, value)

expect(result).to.deep.equal(expected)
})

it('throws a NoSupportedRendererError if there are no existing renderers to support the requested format', async function() {

await expect(this.renderer.renderObject(HalMimeTypes.halXml, { foo: 'bar' })).to.be.rejectedWith(NoSupportedRendererError)
await expect(this.renderer.renderControllerResult(HalMimeTypes.halXml, { foo: 'bar' })).to.be.rejectedWith(NoSupportedRendererError)

})

Expand Down
6 changes: 3 additions & 3 deletions packages/dandi/mvc-hal/src/hal-object-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Inject, Injectable, Optional, Resolver } from '@dandi/core'
import {
ConfiguredObjectRenderer,
ConfiguredObjectRenderer, ControllerResult,
ObjectRenderer,
ObjectRendererBase,
ObjectRendererConfig,
Expand Down Expand Up @@ -57,15 +57,15 @@ export class HalObjectRenderer extends ObjectRendererBase {
}, new Map<string, ObjectRenderer>())
}

public async renderObject(contentType: string, value: any): Promise<string> {
public async renderControllerResult(contentType: string, controllerResult: ControllerResult): Promise<string> {
await this.ready
const renderer = this.renderers.get(contentType)
if (!renderer) {
throw new NoSupportedRendererError(contentType)
}
const halMimeType = parseMimeTypes(contentType)[0]
const subRendererMimeType = parseMimeTypes(`${halMimeType.type}/${halMimeType.subtypeBase}`)
const result = await renderer.render(subRendererMimeType, value)
const result = await renderer.render(subRendererMimeType, controllerResult)
return result.renderedOutput
}

Expand Down
6 changes: 3 additions & 3 deletions packages/dandi/mvc-hal/src/hal.result.transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class HalResultTransformer implements ControllerResultTransformer {
) {}

public async transform(result: ControllerResult): Promise<ControllerResult> {
if (!Resource.isResource(result.resultObject)) {
if (!Resource.isResource(result.data)) {
return result
}

Expand All @@ -29,11 +29,11 @@ export class HalResultTransformer implements ControllerResultTransformer {
)
return Disposable.useAsync(context, async () => {
const resource = await this.composer.compose(
result.resultObject,
result.data,
context,
)
return {
resultObject: resource,
data: resource,
headers: result.headers,
}
})
Expand Down
2 changes: 1 addition & 1 deletion packages/dandi/mvc-view/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from './src/mvc-view.module'
export * from './src/mvc-view-renderer'
export * from './src/render-options'
export * from './src/view.controller-result-transformer'
export * from './src/view.decorator'
export * from './src/view-engine'
export * from './src/view-engine-resolver'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { ProviderOptions } from '@dandi/core'
import { ControllerResultTransformer } from '@dandi/mvc'
import { ViewControllerResultTransformer, ViewResult } from '@dandi/mvc-view'
import { MvcViewRenderer, ViewResult } from '@dandi/mvc-view'

import { stub } from 'sinon'
import { expect } from 'chai'

describe('ViewControllerResultTransformer', function() {
xdescribe('ViewControllerResultTransformer', function() {
beforeEach(function() {
this.viewResult = stub()
this.transformer = new ViewControllerResultTransformer(this.viewResult)
this.transformer = new MvcViewRenderer(this.viewResult)
})

it('is decorated with @Injectable(ControllerResultTransformer)', function() {
expect(Reflect.get(ViewControllerResultTransformer, ProviderOptions.valueOf() as symbol).provide).to.equal(
expect(Reflect.get(MvcViewRenderer, ProviderOptions.valueOf() as symbol).provide).to.equal(
ControllerResultTransformer,
)
})
Expand Down
37 changes: 37 additions & 0 deletions packages/dandi/mvc-view/src/mvc-view-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Disposable } from '@dandi/common'
import { Inject, Injectable, Optional, Resolver } from '@dandi/core'
import {
ControllerResult,
MimeTypes,
ObjectRenderer,
ObjectRendererBase,
ObjectRendererConfig,
parseMimeTypes,
} from '@dandi/mvc'
import { ViewResult } from '@dandi/mvc-view'

import { ViewResultFactory } from './view-result-factory'

@Injectable(ObjectRenderer)
export class MvcViewRenderer extends ObjectRendererBase {

protected readonly defaultContentType: string = MimeTypes.textHtml

constructor(
@Inject(Resolver) private resolver: Resolver,
@Inject(ObjectRendererConfig(MvcViewRenderer)) @Optional() config?: ObjectRendererConfig,
) {
super(parseMimeTypes(MimeTypes.textHtml), config)
}

protected async renderControllerResult(contentType: string, controllerResult: ControllerResult): Promise<string> {
if (controllerResult instanceof ViewResult) {
return controllerResult.value
}
return Disposable.useAsync(await this.resolver.resolve(ViewResultFactory), async factoryResult => {
const factory = factoryResult.singleValue
const viewResult = await factory(undefined, controllerResult.data)
return viewResult.value
})
}
}
6 changes: 4 additions & 2 deletions packages/dandi/mvc-view/src/mvc-view.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Constructor } from '@dandi/common'
import { ModuleBuilder, Registerable } from '@dandi/core'
import { ROUTES_PROVIDER } from '@dandi/mvc'

import { PKG } from './local.token'
import { ConfiguredViewEngine, ViewEngine } from './view-engine'
import { ViewEngineConfig } from './view-engine-config'
import { ViewEngineResolver } from './view-engine-resolver'
import { ViewControllerResultTransformer } from './view.controller-result-transformer'
import { MvcViewRenderer } from './mvc-view-renderer'
import { ViewRouteTransformer } from './view.route-transformer'
import { VIEW_RESULT_FACTORY } from './view-result-factory'

Expand Down Expand Up @@ -36,8 +37,9 @@ export class MvcViewModuleBuilder extends ModuleBuilder<MvcViewModuleBuilder> {
}

export const MvcViewModule = new MvcViewModuleBuilder(
ROUTES_PROVIDER,
VIEW_RESULT_FACTORY,
ViewControllerResultTransformer,
MvcViewRenderer,
ViewEngineResolver,
ViewRouteTransformer,
)
2 changes: 1 addition & 1 deletion packages/dandi/mvc-view/src/view-result-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,6 @@ describe('ViewResultFactory', function() {
expect(viewResult.viewEngine).to.equal(engine)
expect(viewResult.view).to.equal(view)
expect(viewResult.templatePath).to.equal(templatePath)
expect(viewResult.resultObject).to.equal(data)
expect(viewResult.data).to.equal(data)
})
})
6 changes: 3 additions & 3 deletions packages/dandi/mvc-view/src/view-result.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ describe('ViewResult', function() {
}
this.view = {}
this.templatePath = '/template/path'
this.resultObject = {}
this.viewResult = new ViewResult(this.viewEngine, this.view, this.templatePath, this.resultObject)
this.data = {}
this.viewResult = new ViewResult(this.viewEngine, this.view, this.templatePath, this.data)
})

describe('get value()', function() {
it('calls render() on the provided ViewEngine instance and returns its result', async function() {
const result = await this.viewResult.value
expect(this.viewEngine.render).to.have.been.calledWithExactly(this.view, this.templatePath, this.resultObject)
expect(this.viewEngine.render).to.have.been.calledWithExactly(this.view, this.templatePath, this.data)
expect(result).to.equal('some html')
})
})
Expand Down
9 changes: 3 additions & 6 deletions packages/dandi/mvc-view/src/view-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@ import { ControllerResult } from '@dandi/mvc'
import { ViewEngine } from './view-engine'
import { ViewMetadata } from './view-metadata'

const CONTENT_TYPE = 'text/html'

export class ViewResult implements ControllerResult {
public readonly contentType: string = CONTENT_TYPE;

private _value: string | Promise<string>;
private _value: string | Promise<string>
public get value(): string | Promise<string> {
if (!this._value) {
this._value = this.viewEngine.render(this.view, this.templatePath, this.resultObject)
this._value = this.viewEngine.render(this.view, this.templatePath, this.data)
}
return this._value
}
Expand All @@ -20,6 +17,6 @@ export class ViewResult implements ControllerResult {
private readonly viewEngine: ViewEngine,
private readonly view: ViewMetadata,
private readonly templatePath: string,
public readonly resultObject: any,
public readonly data: any,
) {}
}
17 changes: 0 additions & 17 deletions packages/dandi/mvc-view/src/view.controller-result-transformer.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/dandi/mvc/src/controller.result.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export interface ControllerResult {
readonly resultObject: object
readonly data: object
readonly headers?: { [key: string]: string }
}

export function isControllerResult(obj: any): obj is ControllerResult {
return obj && typeof obj.resultObject !== 'undefined'
return obj && typeof obj.data !== 'undefined'
}
10 changes: 5 additions & 5 deletions packages/dandi/mvc/src/default.route.handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,11 @@ describe('DefaultRouteHandler', function() {
await this.invokeHandler()

expect(spy).to.have.been.called
expect(this.renderer.render).to.have.been.calledWith(parseMimeTypes(MimeTypes.applicationJson), { foo: 'yeah!' })
expect(this.renderer.render).to.have.been.calledWith(parseMimeTypes(MimeTypes.applicationJson), { data: { foo: 'yeah!' } })
})

it('adds any response headers specified by the controller result', async function() {
const result = { resultObject: { foo: 'yeah!' }, headers: { 'x-fizzle-bizzle': 'okay' } }
const result = { data: { foo: 'yeah!' }, headers: { 'x-fizzle-bizzle': 'okay' } }
class TestController {
public async method(): Promise<any> {
return result
Expand All @@ -160,7 +160,7 @@ describe('DefaultRouteHandler', function() {
})

it('sets the contentType using the renderer result', async function() {
const result = { resultObject: { foo: 'yeah!' }, headers: { 'x-fizzle-bizzle': 'okay' } }
const result = { data: { foo: 'yeah!' }, headers: { 'x-fizzle-bizzle': 'okay' } }
class TestController {
public async method(): Promise<any> {
return result
Expand All @@ -178,7 +178,7 @@ describe('DefaultRouteHandler', function() {

it('calls res.send() with the rendered output of the renderer result', async function() {

const result = { resultObject: { foo: 'yeah!' }, headers: { 'x-fizzle-bizzle': 'okay' } }
const result = { data: { foo: 'yeah!' }, headers: { 'x-fizzle-bizzle': 'okay' } }
class TestController {
public async method(): Promise<any> {
return result
Expand All @@ -203,7 +203,7 @@ describe('DefaultRouteHandler', function() {
}
this.registerController(new TestController())

expect(this.invokeHandler())
await expect(this.invokeHandler())
.to.be.rejectedWith(MissingPathParamError)

})
Expand Down
Loading

0 comments on commit 4e307da

Please sign in to comment.