Skip to content

Commit

Permalink
sdk: add automatic feature loader rucio#171
Browse files Browse the repository at this point in the history
  • Loading branch information
maany committed Jun 21, 2023
1 parent 4f8d245 commit 2e42040
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 52 deletions.
26 changes: 2 additions & 24 deletions src/lib/infrastructure/ioc/container-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ import UserPassLoginInputPort from "@/lib/core/port/primary/userpass-login-input
import UserPassLoginUseCase from "@/lib/core/use-case/userpass-login-usecase";
import UserPassLoginController, {IUserPassLoginController} from "@/lib/infrastructure/controller/userpass-login-controller";
import UserPassLoginPresenter from "@/lib/infrastructure/presenter/usepass-login-presenter";
import LoginConfigUseCase from "@/lib/core/use-case/login-config-usecase";
import LoginConfigPresenter from "@/lib/infrastructure/presenter/login-config-presenter";
import LoginConfigController from "@/lib/infrastructure/controller/login-config-controller";
import SetX509LoginSessionInputPort from "@/lib/core/port/primary/set-x509-login-session-input-port";
import SetX509LoginSessionUseCase from "@/lib/core/use-case/set-x509-login-session-usecase";
import SetX509LoginSessionController, { ISetX509LoginSessionController } from "../controller/set-x509-login-session-controller";
Expand All @@ -46,16 +43,13 @@ import ListDIDsController, { ListDIDsControllerParameters } from "../controller/
import ListDIDsPresenter from "../presenter/list-dids-presenter";
import { ListDIDsRequest } from "@/lib/core/usecase-models/list-dids-usecase-models";
import { BaseController } from "@/lib/sdk/controller";
import { createIOCBindings, IOCSymbols } from "@/lib/sdk/ioc-helpers";
import { loadFeatures } from "@/lib/sdk/ioc-helpers";


/**
* IoC Container configuration for the application.
*/
const appContainer = new Container();
// const INPUT_PORT = getInputPorts();
// console.log(INPUT_PORT)
// console.log(CONTROLLERS)
appContainer.bind<AccountGatewayOutputPort>(GATEWAYS.ACCOUNT).to(RucioAccountGateway);
appContainer.bind<AuthServerGatewayOutputPort>(GATEWAYS.AUTH_SERVER).to(RucioAuthServer);
appContainer.bind<DIDGatewayOutputPort>(GATEWAYS.DID).to(RucioDIDGateway);
Expand All @@ -81,23 +75,7 @@ appContainer.bind<interfaces.Factory<ListDIDsInputPort>>(USECASE_FACTORY.LIST_DI
}
);

// appContainer.bind<LoginConfigInputPort>(INPUT_PORT.LOGIN_CONFIG).to(LoginConfigUseCase).inRequestScope();
// appContainer.bind<ILoginConfigController>(CONTROLLERS.LOGIN_CONFIG).to(LoginConfigController);
// appContainer.bind<interfaces.Factory<LoginConfigInputPort>>(USECASE_FACTORY.LOGIN_CONFIG).toFactory<LoginConfigUseCase, [IronSession, NextApiResponse]>((context: interfaces.Context) =>
// (session: IronSession, response: NextApiResponse) => {
// const envConfigGateway: EnvConfigGatewayOutputPort = appContainer.get(GATEWAYS.ENV_CONFIG)
// return new LoginConfigUseCase(new LoginConfigPresenter(session, response), envConfigGateway);
// }
// );
createIOCBindings(appContainer, LoginConfigController, LoginConfigUseCase, [
appContainer.get<EnvConfigGatewayOutputPort>(GATEWAYS.ENV_CONFIG)
], LoginConfigPresenter, true, {
CONTROLLER: CONTROLLERS.LOGIN_CONFIG,
INPUT_PORT: INPUT_PORT.LOGIN_CONFIG,
USECASE_FACTORY: USECASE_FACTORY.LOGIN_CONFIG,

} as IOCSymbols)

loadFeatures(appContainer)

appContainer.bind<SetX509LoginSessionInputPort>(INPUT_PORT.SET_X509_LOGIN_SESSION).to(SetX509LoginSessionUseCase).inRequestScope();
appContainer.bind<ISetX509LoginSessionController>(CONTROLLERS.SET_X509_LOGIN_SESSION).to(SetX509LoginSessionController);
Expand Down
44 changes: 44 additions & 0 deletions src/lib/infrastructure/ioc/features/logic-config-feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import EnvConfigGatewayOutputPort from "@/lib/core/port/secondary/env-config-gateway-output-port"
import LoginConfigUseCase from "@/lib/core/use-case/login-config-usecase"
import { LoginConfigError, LoginConfigResponse } from "@/lib/core/usecase-models/login-config-usecase-models"
import { BaseFeature, IOCSymbols } from "@/lib/sdk/ioc-helpers"
import { Container } from "inversify"
import LoginConfigController, { LoginConfigControllerParams } from "../../controller/login-config-controller"
import { LoginViewModel } from "../../data/view-model/login"
import EnvConfigGateway from "../../gateway/env-config-gateway"
import LoginConfigPresenter from "../../presenter/login-config-presenter"
import CONTROLLERS from "../ioc-symbols-controllers"
import GATEWAYS from "../ioc-symbols-gateway"
import INPUT_PORT from "../ioc-symbols-input-port"
import USECASE_FACTORY from "../ioc-symbols-usecase-factory"

export default class LoginConfigFeature extends BaseFeature<
LoginConfigControllerParams,
void,
LoginConfigResponse,
LoginConfigError,
LoginViewModel
> {
constructor(
appContainer: Container,

) {
const envGateway: EnvConfigGateway = appContainer.get<EnvConfigGatewayOutputPort>(GATEWAYS.ENV_CONFIG)
const symbols: IOCSymbols = {
CONTROLLER: CONTROLLERS.LOGIN_CONFIG,
USECASE_FACTORY: USECASE_FACTORY.LOGIN_CONFIG,
INPUT_PORT: INPUT_PORT.LOGIN_CONFIG,
}
super(
appContainer,
LoginConfigController,
LoginConfigUseCase,
[
envGateway,
],
LoginConfigPresenter,
true,
symbols
)
}
}
136 changes: 111 additions & 25 deletions src/lib/sdk/ioc-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,100 @@ import { Container, interfaces } from 'inversify';
import { IronSession } from 'iron-session';
import { NextApiResponse } from 'next';
import path from 'path';
import INPUT_PORT from '../infrastructure/ioc/ioc-symbols-input-port';
import { BaseController, TParameters } from './controller';
import { BasePresenter } from './presenter';
import type { BaseInputPort, BaseOutputPort } from './primary-ports';
import { BaseUseCase, TUseCase } from './usecase';
import { TUseCase } from './usecase';
import TUseCaseFactory from './usecase-factory';

/**
* An object that contains symbols for the different types of dependencies in an IoC container.
*/
export type IOCSymbols = {
CONTROLLER: symbol,
USECASE_FACTORY: symbol,
INPUT_PORT: symbol,
CONTROLLER: symbol, // A symbol for the controller dependency.
USECASE_FACTORY: symbol, // A symbol for the use case factory dependency.
INPUT_PORT: symbol, // A symbol for the input port dependency.
}

/**
* A base class for features in a web application. The IOC bindings for the clean architecture components
* of the feature are generated automatically.
* @template TControllerParams The type of the parameters for the controller.
* @template TRequestModel The type of the request model for the use case.
* @template TResponseModel The type of the response model for the use case.
* @template TErrorModel The type of the error model for the use case.
* @template TViewModel The type of the view model for the presenter.
*/
export class BaseFeature<
TControllerParams extends TParameters,
TRequestModel,
TResponseModel,
TErrorModel,
TViewModel,
>
{
/**
* Creates a new instance of the `BaseFeature` class.
* @template TControllerParams The type of the parameters for the controller.
* @template TRequestModel The type of the request model for the use case.
* @template TResponseModel The type of the response model for the use case.
* @template TErrorModel The type of the error model for the use case.
* @template TViewModel The type of the view model for the presenter.
* @param appContainer The IoC container for the application.
* @param Controller The controller class for the feature.
* @param UseCase The use case class for the feature.
* @param useCaseContructorArgs The arguments to pass to the use case constructor.
* @param Presenter The presenter class for the feature.
* @param passSessionToPresenter Whether to pass the session to the presenter.
* @param symbols An object that contains symbols for the different types of dependencies in the IoC container.
*/
public constructor(
appContainer: Container,
Controller: new (useCaseFactory: TUseCaseFactory<TRequestModel>) => BaseController<TControllerParams, TRequestModel>,
UseCase: new (presenter: BaseOutputPort<TResponseModel, TErrorModel>, ...args: any[]) => TUseCase<TRequestModel>,
useCaseContructorArgs: any[] = [],
Presenter: new (response: NextApiResponse, session?: IronSession) => BasePresenter<TResponseModel, TErrorModel, TViewModel>,
passSessionToPresenter: boolean = false,
symbols: IOCSymbols,
) {
this.createIOCBindings<TControllerParams, TRequestModel, TResponseModel, TErrorModel, TViewModel>(
appContainer,
Controller,
UseCase,
useCaseContructorArgs,
Presenter,
passSessionToPresenter,
symbols,
)
}


export function createIOCBindings<
/**
* Creates IoC bindings for a feature.
* @template TControllerParams The type of the parameters for the controller.
* @template TRequestModel The type of the request model for the use case.
* @template TResponseModel The type of the response model for the use case.
* @template TErrorModel The type of the error model for the use case.
* @template TViewModel The type of the view model for the presenter.
* @param appContainer The IoC container for the application.
* @param Controller The controller class for the feature.
* @param UseCase The use case class for the feature.
* @param useCaseContructorArgs The arguments to pass to the use case constructor.
* @param Presenter The presenter class for the feature.
* @param passSessionToPresenter Whether to pass the session to the presenter.
* @param symbols An object that contains symbols for the different types of dependencies in the IoC container.
*/
createIOCBindings<
TControllerParams extends TParameters,
TRequestModel,
TResponseModel,
TErrorModel,
TOutput_Port extends new (response: NextApiResponse, session?: IronSession) => BaseOutputPort<any, any>,
TViewModel,
>(
appContainer: Container,
Controller: new (useCaseFactory: TUseCaseFactory<TRequestModel>) => BaseController<TControllerParams, TRequestModel>,
UseCase: new (presenter: BaseOutputPort<TResponseModel, TErrorModel>, ...args: any[]) => TUseCase<TRequestModel>,
useCaseContructorArgs: any[] = [],
Presenter: TOutput_Port,
Presenter: new (response: NextApiResponse, session?: IronSession) => BasePresenter<TResponseModel, TErrorModel, TViewModel>,
passSessionToPresenter: boolean = false,
symbols: IOCSymbols,

Expand Down Expand Up @@ -56,23 +124,41 @@ export function createIOCBindings<
);
}
}

}

/**
* @deprecated
* Loads features from the features directory into the IoC Container.
* @param appContainer The IoC container for the application.
* @param featuresDir The directory to load features from. Defaults to `src/lib/infrastructure/ioc/features`.
*/
export async function loadFeature(feature: string, appContainer: Container) {
const CONTROLLER = path.join(process.cwd(), 'src', 'lib', 'infrastructure', 'controller');
const USECASE = path.join(process.cwd(), 'src', 'lib', 'core', 'use-case');
const PORTS = path.join(process.cwd(), 'src', 'lib', 'core', 'port', 'primary');
const controllerModule = await import(`${CONTROLLER}/${feature}-controller`);
const usecaseModule = await import(`${USECASE}/${feature}-usecase`);
const portsModule = await import(`${PORTS}/${feature}-ports`);

const { default: defaultController, regularController } = controllerModule;
const usecase = usecaseModule.default;
const { InputPort, OutputPort } = portsModule;
appContainer.bind('Controller').toConstantValue(controllerModule);
// createIOCBindings<any, typeof InputPort, typeof usecase, typeof OutputPort, typeof regularController>(appContainer, defaultController, usecase, [], OutputPort, false);
INPUT_PORT
export async function loadFeatures(appContainer: Container, featuresDir?: string) {
const FEATURES_PATH = featuresDir || path.join(process.cwd(), 'src', 'lib', 'infrastructure', 'ioc', 'features');
// scan for features
const features = fs.readdirSync(FEATURES_PATH);
console.log(`Found ${features.length} features`)
for (const feature of features) {
const featureName = feature.split('.')[0];
console.log(`Loading feature ${feature}`)
const featureModule = await import(`${FEATURES_PATH}/${feature}`);
const featureClass = featureModule.default;
// if no default export, throw error
if (!featureClass) {
throw new Error(`Feature ${featureName} has no default export`)
}
// if default export is not a subclass of BaseFeature, throw error
if (!(featureClass.prototype instanceof BaseFeature)) {
throw new Error(`Feature ${featureName} is not a subclass of BaseFeature`)
}
// if constructor signature of default export is not new (appContainer: Container) => BaseFeature, throw error
if (featureClass.length !== 1) {
throw new Error(`Feature ${featureName} does not have a constructor signature of new (appContainer: Container) => BaseFeature`)
}
// create instance of feature
try {
const featureInstance = new featureClass(appContainer);
} catch (error) {
console.error(`Error loading feature ${featureName}: ${error}`)
throw error;
}
}
}
1 change: 0 additions & 1 deletion src/lib/sdk/primary-ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export interface BaseStreamableInputPort<AuthenticatedRequestModel>
/**
* A base interface for output ports.
* @typeparam TResponseModel The type of the response model for the output port.
* @typeparam TViewModel The type of the view model for the output port.
* @typeparam TErrorModel The type of the error model for the output port.
*/
export interface BaseOutputPort<TResponseModel, TErrorModel> {
Expand Down
6 changes: 4 additions & 2 deletions test/api/auth/login-page-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { LoginViewModel } from "@/lib/infrastructure/data/view-model/login";
import { getIronSession } from "iron-session";
import { createMocks } from "node-mocks-http";
import { createOIDCProviders, deleteOIDCProviders } from "test/fixtures/oidc-provider-config";
import { BaseController } from "@/lib/sdk/controller";
import { LoginConfigControllerParams } from "@/lib/infrastructure/controller/login-config-controller";

describe('Login Page Config API Test', () => {
beforeEach(() => {
Expand All @@ -28,10 +30,10 @@ describe('Login Page Config API Test', () => {
})
await setEmptySession(session, true)

const loginConfigController = appContainer.get<ILoginConfigController>(CONTROLLERS.LOGIN_CONFIG)
const loginConfigController = appContainer.get<BaseController<LoginConfigControllerParams, void>>(CONTROLLERS.LOGIN_CONFIG)
await loginConfigController.execute({
session: session,
response: res
response: res as any,
});

expect(res._getStatusCode()).toBe(200);
Expand Down

0 comments on commit 2e42040

Please sign in to comment.