Skip to content

feat: add support for custom deserializers #113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@
"name": "vscode-jest-tests",
"request": "launch",
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
"args": ["--runInBand", "--config", "${workspaceFolder}/projects/hyperdash-angular/jest.config.debug.js"],
"args": ["--runInBand", "--config", "${workspaceFolder}/jest.config.debug.js"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
4 changes: 2 additions & 2 deletions angular.json
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
@@ -84,7 +84,7 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
"tsConfig": ["tsconfig.app.json", "tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
}
11 changes: 11 additions & 0 deletions jest.config.debug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
preset: 'jest-preset-angular',
modulePathIgnorePatterns: ['<rootDir>/dist/'],
watchPathIgnorePatterns: ['test-results'],
moduleNameMapper: {
'^lodash-es$': 'lodash',
'^@hypertrace/hyperdash-angular$': '<rootDir>/projects/hyperdash-angular/src/public_api.ts'
},
setupFilesAfterEnv: ['<rootDir>/node_modules/@angular-builders/jest/dist/jest-config/setup.js'],
testMatch: ['<rootDir>/**/+(*.)+(spec|test).+(ts|js)?(x)']
};
5 changes: 0 additions & 5 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
module.exports = {
globals: {
'ts-jest': {
tsConfig: '<rootDir>/src/tsconfig.spec.json'
}
},
reporters: [
'default',
[
34,105 changes: 34,037 additions & 68 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@
"@angular/platform-browser": "^11.0.2",
"@angular/platform-browser-dynamic": "^11.0.2",
"@angular/router": "^11.0.2",
"@hypertrace/hyperdash": "^1.1.2",
"@hypertrace/hyperdash": "^1.2.0",
"core-js": "^3.7.0",
"lodash-es": "^4.17.15",
"rxjs": "^6.6.3",
11 changes: 0 additions & 11 deletions projects/hyperdash-angular/jest.config.debug.js

This file was deleted.

2 changes: 1 addition & 1 deletion projects/hyperdash-angular/package.json
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@
"rxjs": "^6.6.3",
"zone.js": "~0.10.3",
"lodash-es": "^4.17.15",
"@hypertrace/hyperdash": "^1.1.2"
"@hypertrace/hyperdash": "^1.2.0"
},
"dependencies": {
"tslib": "^2.0.3"
Original file line number Diff line number Diff line change
@@ -2,20 +2,18 @@ import { Injectable } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
ARRAY_PROPERTY,
BOOLEAN_PROPERTY,
Deserializer,
JsonPrimitive,
LogMessage,
ModelPropertyTypeRegistrationInformation,
NUMBER_PROPERTY
ModelPropertyTypeRegistrationInformation
} from '@hypertrace/hyperdash';
import { DeserializationManagerService } from '../injectable-wrappers/deserialization/deserialization-manager.service';
import { LoggerService } from '../injectable-wrappers/logger.service';
import { ModelLibraryService } from '../injectable-wrappers/model-library.service';
import { ModelManagerService } from '../injectable-wrappers/model-manager.service';
import { ModelPropertyTypeLibraryService } from '../injectable-wrappers/model-property-type-library.service';
import { ModelPropertyValidatorService } from '../injectable-wrappers/model-property-validator.service';
import { SerializationManagerService } from '../injectable-wrappers/serialization/serialization-manager.service';
import { VariableManagerService } from '../injectable-wrappers/variable-manager.service';
import { MODEL_PROPERTY_TYPES } from '../module/dashboard-core.module';
import { DASHBOARD_DESERIALIZERS, MODEL_PROPERTY_TYPES } from '../module/dashboard-core.module';
import { DefaultConfigurationService } from './default-configuration.service';

describe('Default configuration service', () => {
@@ -29,12 +27,17 @@ describe('Default configuration service', () => {
provide: MODEL_PROPERTY_TYPES,
useValue: [{ type: 'test-property' }, TestPropertyTypeProvider],
multi: true
},
{
provide: DASHBOARD_DESERIALIZERS,
useValue: [TestDeserializer],
multi: true
}
]
});

defaultConfigurationService = TestBed.get(DefaultConfigurationService);
logger = TestBed.get(LoggerService);
defaultConfigurationService = TestBed.inject(DefaultConfigurationService);
logger = TestBed.inject(LoggerService);
const errorSpy = jest.spyOn(logger, 'error');
errorSpy.mockImplementation(
message =>
@@ -48,96 +51,10 @@ describe('Default configuration service', () => {
logger.warn = jest.fn();
});

test('correctly configures deserialization', () => {
const deserializationManager = TestBed.get(DeserializationManagerService) as DeserializationManagerService;
const modelLibrary = TestBed.get(ModelLibraryService) as ModelLibraryService;

(TestBed.get(ModelPropertyValidatorService) as ModelPropertyValidatorService).setStrictSchema(false);
const testModel = class ModelClass {
public constructor(public prop: unknown) {}
};

modelLibrary.registerModelClass(testModel, { type: 'test-model' });
modelLibrary.registerModelProperty(testModel, 'prop', {
type: BOOLEAN_PROPERTY.type,
key: 'prop'
});

// Should throw until we configure the deserialization
expect(() => deserializationManager.deserialize({ type: 'test-model', prop: false })).toThrow();

defaultConfigurationService.configure();
expect(deserializationManager.deserialize({ type: 'test-model', prop: false })).toEqual(new testModel(false));
expect(deserializationManager.deserialize({ type: 'test-model', prop: [false] })).toEqual(new testModel([false]));
expect(deserializationManager.deserialize({ type: 'test-model', prop: { nested: false } })).toEqual(
new testModel({ nested: false })
);

expect(
deserializationManager.deserialize({
type: 'test-model',
prop: {
type: 'test-model',
prop: 'two models'
}
})
).toEqual(new testModel(new testModel('two models')));

expect(
deserializationManager.deserialize({
type: 'test-model',
prop: {
nested: {
type: 'test-model',
prop: 'object sandwich'
}
}
})
).toEqual(new testModel({ nested: new testModel('object sandwich') }));
});

test('correctly configures deserialization and setting of variables', () => {
const deserializationManager = TestBed.get(DeserializationManagerService) as DeserializationManagerService;
const modelLibrary = TestBed.get(ModelLibraryService) as ModelLibraryService;

const testModel = class ModelClass {
public constructor(public prop?: number) {}
};

modelLibrary.registerModelClass(testModel, { type: 'test-model' });
modelLibrary.registerModelProperty(testModel, 'prop', {
type: NUMBER_PROPERTY.type,
key: 'prop',
required: false
});

defaultConfigurationService.configure();

const deserializedModel = deserializationManager.deserialize<object>({
type: 'test-model',
// tslint:disable-next-line:no-invalid-template-strings
prop: '${test}'
});

expect(deserializedModel).toEqual(new testModel());

(TestBed.get(VariableManagerService) as VariableManagerService).set('test', 42, deserializedModel);

expect(deserializedModel).toEqual(new testModel(42));
});

test('should throw if attempting to configure twice', () => {
defaultConfigurationService.configure();

expect(() => defaultConfigurationService.configure()).toThrow(
'Default Configuration Service cannot be configured twice'
);
});

test('correctly configures serialization', () => {
const serializationManager = TestBed.get(SerializationManagerService) as SerializationManagerService;
const modelLibrary = TestBed.get(ModelLibraryService) as ModelLibraryService;
const modelManager = TestBed.get(ModelManagerService) as ModelManagerService;
const serializationManager = TestBed.inject(SerializationManagerService);
const modelLibrary = TestBed.inject(ModelLibraryService);
const modelManager = TestBed.inject(ModelManagerService);

const testModel = class {
public constructor(public prop?: unknown[]) {}
@@ -170,12 +87,29 @@ describe('Default configuration service', () => {
});

test('registers provided property types', () => {
const propertyTypeLibrary = TestBed.get(ModelPropertyTypeLibraryService) as ModelPropertyTypeLibraryService;
const propertyTypeLibrary = TestBed.inject(ModelPropertyTypeLibraryService);
propertyTypeLibrary.registerPropertyType = jest.fn();
defaultConfigurationService.configure();
expect(propertyTypeLibrary.registerPropertyType).toHaveBeenCalledWith({ type: 'test-property' });

expect(propertyTypeLibrary.registerPropertyType).toHaveBeenCalledTimes(2);
expect(propertyTypeLibrary.registerPropertyType).toHaveBeenCalledWith({ type: 'test-property' });
expect(propertyTypeLibrary.registerPropertyType).toHaveBeenCalledWith(expect.any(TestPropertyTypeProvider));

defaultConfigurationService.configure();
// Should not be called a third time
expect(propertyTypeLibrary.registerPropertyType).toHaveBeenCalledTimes(2);
});

test('registers provided deserializers', () => {
const deserializationManager = TestBed.inject(DeserializationManagerService);
deserializationManager.registerDeserializer = jest.fn();
defaultConfigurationService.configure();
expect(deserializationManager.registerDeserializer).toHaveBeenCalledTimes(1);
expect(deserializationManager.registerDeserializer).toHaveBeenCalledWith(expect.any(TestDeserializer));

defaultConfigurationService.configure();
// Should not be called a second time
expect(deserializationManager.registerDeserializer).toHaveBeenCalledTimes(1);
});
});

@@ -185,3 +119,15 @@ describe('Default configuration service', () => {
class TestPropertyTypeProvider implements ModelPropertyTypeRegistrationInformation {
public readonly type: string = 'test-prop-provider';
}

@Injectable({
providedIn: 'root'
})
class TestDeserializer implements Deserializer<string, string> {
public canDeserialize(json: JsonPrimitive): json is string {
return typeof json === 'string';
}
public deserialize(json: string): string {
return json.toUpperCase();
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { Inject, Injectable, Injector, Type } from '@angular/core';
import { ModelPropertyTypeRegistrationInformation } from '@hypertrace/hyperdash';
import { AbstractType, Inject, Injectable, Injector, Type } from '@angular/core';
import { Deserializer, ModelPropertyTypeRegistrationInformation } from '@hypertrace/hyperdash';
import { flatten, uniq } from 'lodash-es';
import { DefaultModelApiBuilderService } from '../injectable-wrappers/default-model-api-builder.service';
import { ArrayDeserializerService } from '../injectable-wrappers/deserialization/array-deserializer.service';
import { DeserializationManagerService } from '../injectable-wrappers/deserialization/deserialization-manager.service';
import { ModelDeserializerService } from '../injectable-wrappers/deserialization/model-deserializer.service';
import { ObjectDeserializerService } from '../injectable-wrappers/deserialization/object-deserializer.service';
import { PrimitiveDeserializerService } from '../injectable-wrappers/deserialization/primitive-deserializer.service';
import { VariableDeserializerService } from '../injectable-wrappers/deserialization/variable-deserializer.service';
import { ModelEventInstallerService } from '../injectable-wrappers/model-event-installer.service';
import { ModelManagerService } from '../injectable-wrappers/model-manager.service';
import { ModelPropertyTypeLibraryService } from '../injectable-wrappers/model-property-type-library.service';
@@ -18,7 +13,7 @@ import { PrimitiveSerializerService } from '../injectable-wrappers/serialization
import { SerializationManagerService } from '../injectable-wrappers/serialization/serialization-manager.service';
import { VariableSerializerService } from '../injectable-wrappers/serialization/variable-serializer.service';
import { ModelInjectService } from '../model/decorators/model-inject.service';
import { MODEL_PROPERTY_TYPES } from '../module/dashboard-core.module';
import { DASHBOARD_DESERIALIZERS, MODEL_PROPERTY_TYPES } from '../module/dashboard-core.module';

/**
* Configures dashboard services for default behavior
@@ -28,14 +23,10 @@ import { MODEL_PROPERTY_TYPES } from '../module/dashboard-core.module';
})
export class DefaultConfigurationService {
private configured: boolean = false;
private readonly registeredObjects: WeakSet<object> = new WeakSet();

public constructor(
private readonly deserializationManager: DeserializationManagerService,
private readonly objectDeserializer: ObjectDeserializerService,
private readonly arrayDeserializer: ArrayDeserializerService,
private readonly primitiveDeserializer: PrimitiveDeserializerService,
private readonly modelDeserializer: ModelDeserializerService,
private readonly variableDeserializer: VariableDeserializerService,
private readonly modelManager: ModelManagerService,
private readonly defaultModelApiBuilder: DefaultModelApiBuilderService,
private readonly serializationManager: SerializationManagerService,
@@ -48,35 +39,43 @@ export class DefaultConfigurationService {
private readonly modelEventInstaller: ModelEventInstallerService,
private readonly modelInjectService: ModelInjectService,
private readonly injector: Injector,
@Inject(MODEL_PROPERTY_TYPES) private readonly propertyTypes: PropertyTypeRegistration[][]
@Inject(MODEL_PROPERTY_TYPES) private readonly propertyTypes: PropertyTypeRegistration[][],
@Inject(DASHBOARD_DESERIALIZERS)
private readonly deserializers: (Type<Deserializer> | AbstractType<Deserializer>)[][]
) {}

/**
* Does the configuration. This should be called during application bootstrap.
* Does the configuration. This should be called during application bootstrap. Later calls will
* do incremental configuration.
*
* @throws Error if configure is called more than once
*/
public configure(): void {
this.registerPropertyTypes();
this.registerDeserializers();

if (this.configured) {
throw new Error('Default Configuration Service cannot be configured twice');
return;
}
this.configured = true;
this.registerPropertyTypes();
this.configureDeserialization();
// Beyond here, code should only be invoked once
this.configureSerialization();
this.registerModelDecorators();
this.configureModelApiBuilder();
}

private configureDeserialization(): void {
this.deserializationManager.registerDeserializer(this.variableDeserializer);
this.deserializationManager.registerDeserializer(this.primitiveDeserializer);
this.deserializationManager.registerDeserializer(this.modelDeserializer);
this.deserializationManager.registerDeserializer(this.arrayDeserializer);
this.deserializationManager.registerDeserializer(this.objectDeserializer);
private registerDeserializers(): void {
uniq(flatten(this.deserializers)).forEach(deserializerClass => {
if (this.registeredObjects.has(deserializerClass)) {
return;
}
this.registeredObjects.add(deserializerClass);

this.deserializationManager.registerDeserializer(this.injector.get(deserializerClass));
});
}

private configureSerialization(): void {
// TODO - expose customizing serializers similar to deserializers
this.serializationManager.registerSerializer(this.variableSerializer);
this.serializationManager.registerSerializer(this.primitiveSerializer);
this.serializationManager.registerSerializer(this.modelSerializer);
@@ -90,6 +89,11 @@ export class DefaultConfigurationService {

private registerPropertyTypes(): void {
uniq(flatten(this.propertyTypes)).forEach(propertyType => {
if (this.registeredObjects.has(propertyType)) {
return;
}
this.registeredObjects.add(propertyType);

if (typeof propertyType === 'object') {
this.propertyTypeLibrary.registerPropertyType(propertyType);
} else {
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ describe('Model JSON editor component', () => {
imports: [DashboardEditorModule, TestDashboardModule]
});

(TestBed.get(DefaultConfigurationService) as DefaultConfigurationService).configure();
TestBed.inject(DefaultConfigurationService).configure();
host = TestBed.createComponent(HostComponent);
host.detectChanges();
});
@@ -124,7 +124,7 @@ describe('Model JSON editor component', () => {
test('destroys models when a new json object is provided', () => {
const destroySpy = jest.fn();

(TestBed.get(ModelDestroyedEventService) as ModelDestroyedEventService).getObservable().subscribe(destroySpy);
TestBed.inject(ModelDestroyedEventService).getObservable().subscribe(destroySpy);

host.componentInstance.modelJson = {
type: 'test-model',
Original file line number Diff line number Diff line change
@@ -43,11 +43,11 @@ describe('Model editor component', () => {
imports: [DashboardEditorModule, moduleWithEntryComponents(PropEditorComponent)]
});

mockModelEditorService = TestBed.get(ModelEditorService);
mockModelEditorService = TestBed.inject(ModelEditorService);
mockModelEditorService.getRenderData = jest.fn().mockReturnValue([
{
component: PropEditorComponent,
injector: TestBed.get(Injector)
injector: TestBed.inject(Injector)
}
]);

Original file line number Diff line number Diff line change
@@ -78,48 +78,48 @@ import { VariableManagerService } from './variable-manager.service';

describe('Injectable wrappers', () => {
test('should correctly register providers', () => {
expect(TestBed.get(ModelLibraryService) instanceof ModelLibrary).toBeTruthy();
expect(TestBed.get(RendererLibraryService) instanceof RendererLibrary).toBeTruthy();
expect(TestBed.get(ModelDeserializerService) instanceof ModelDeserializer).toBeTruthy();
expect(TestBed.get(ArrayDeserializerService) instanceof ArrayDeserializer).toBeTruthy();
expect(TestBed.get(ObjectDeserializerService) instanceof ObjectDeserializer).toBeTruthy();
expect(TestBed.get(PrimitiveDeserializerService) instanceof PrimitiveDeserializer).toBeTruthy();
expect(TestBed.get(DeserializationManagerService) instanceof DeserializationManager).toBeTruthy();
expect(TestBed.get(ModelManagerService) instanceof ModelManager).toBeTruthy();
expect(TestBed.get(DashboardManagerService) instanceof DashboardManager).toBeTruthy();
expect(TestBed.get(DataSourceManagerService) instanceof DataSourceManagerService).toBeTruthy();
expect(TestBed.get(LoggerService) instanceof Logger).toBeTruthy();
expect(TestBed.get(ModelPropertyTypeLibraryService) instanceof ModelPropertyTypeLibrary).toBeTruthy();
expect(TestBed.get(VariableManagerService) instanceof VariableManager).toBeTruthy();
expect(TestBed.get(VariableDeserializerService) instanceof VariableDeserializer).toBeTruthy();
expect(TestBed.get(DashboardEventManagerService) instanceof DashboardEventManager).toBeTruthy();
expect(TestBed.get(DefaultModelApiBuilderService) instanceof DefaultModelApiBuilder).toBeTruthy();
expect(TestBed.get(ModelCreatedEventService) instanceof ModelCreatedEvent).toBeTruthy();
expect(TestBed.get(ModelDestroyedEventService) instanceof ModelDestroyedEvent).toBeTruthy();
expect(TestBed.get(ModelPropertyValidatorService) instanceof ModelPropertyValidator).toBeTruthy();
expect(TestBed.get(ModelChangedEventService) instanceof ModelChangedEvent).toBeTruthy();
expect(TestBed.get(ThemeManagerService) instanceof ThemeManager).toBeTruthy();
expect(TestBed.get(ModelEventInstallerService) instanceof ModelEventInstaller).toBeTruthy();
expect(TestBed.get(ModelSerializerService) instanceof ModelSerializer).toBeTruthy();
expect(TestBed.get(ArraySerializerService) instanceof ArraySerializer).toBeTruthy();
expect(TestBed.get(ObjectSerializerService) instanceof ObjectSerializer).toBeTruthy();
expect(TestBed.get(PrimitiveSerializerService) instanceof PrimitiveSerializer).toBeTruthy();
expect(TestBed.get(SerializationManagerService) instanceof SerializationManager).toBeTruthy();
expect(TestBed.get(VariableSerializerService) instanceof VariableSerializer).toBeTruthy();
expect(TestBed.get(EditorLibraryService) instanceof EditorLibrary).toBeTruthy();
expect(TestBed.get(EditorApiFactoryService) instanceof EditorApiFactory).toBeTruthy();
expect(TestBed.get(ModelPropertyTypeService) instanceof ModelPropertyType).toBeTruthy();
expect(TestBed.get(DataRefreshEventService) instanceof DataRefreshEvent).toBeTruthy();
expect(TestBed.get(TimeRangeChangedEventService) instanceof TimeRangeChangedEvent).toBeTruthy();
expect(TestBed.get(TimeRangeManagerService) instanceof TimeRangeManager).toBeTruthy();
expect(TestBed.inject(ModelLibraryService) instanceof ModelLibrary).toBeTruthy();
expect(TestBed.inject(RendererLibraryService) instanceof RendererLibrary).toBeTruthy();
expect(TestBed.inject(ModelDeserializerService) instanceof ModelDeserializer).toBeTruthy();
expect(TestBed.inject(ArrayDeserializerService) instanceof ArrayDeserializer).toBeTruthy();
expect(TestBed.inject(ObjectDeserializerService) instanceof ObjectDeserializer).toBeTruthy();
expect(TestBed.inject(PrimitiveDeserializerService) instanceof PrimitiveDeserializer).toBeTruthy();
expect(TestBed.inject(DeserializationManagerService) instanceof DeserializationManager).toBeTruthy();
expect(TestBed.inject(ModelManagerService) instanceof ModelManager).toBeTruthy();
expect(TestBed.inject(DashboardManagerService) instanceof DashboardManager).toBeTruthy();
expect(TestBed.inject(DataSourceManagerService) instanceof DataSourceManagerService).toBeTruthy();
expect(TestBed.inject(LoggerService) instanceof Logger).toBeTruthy();
expect(TestBed.inject(ModelPropertyTypeLibraryService) instanceof ModelPropertyTypeLibrary).toBeTruthy();
expect(TestBed.inject(VariableManagerService) instanceof VariableManager).toBeTruthy();
expect(TestBed.inject(VariableDeserializerService) instanceof VariableDeserializer).toBeTruthy();
expect(TestBed.inject(DashboardEventManagerService) instanceof DashboardEventManager).toBeTruthy();
expect(TestBed.inject(DefaultModelApiBuilderService) instanceof DefaultModelApiBuilder).toBeTruthy();
expect(TestBed.inject(ModelCreatedEventService) instanceof ModelCreatedEvent).toBeTruthy();
expect(TestBed.inject(ModelDestroyedEventService) instanceof ModelDestroyedEvent).toBeTruthy();
expect(TestBed.inject(ModelPropertyValidatorService) instanceof ModelPropertyValidator).toBeTruthy();
expect(TestBed.inject(ModelChangedEventService) instanceof ModelChangedEvent).toBeTruthy();
expect(TestBed.inject(ThemeManagerService) instanceof ThemeManager).toBeTruthy();
expect(TestBed.inject(ModelEventInstallerService) instanceof ModelEventInstaller).toBeTruthy();
expect(TestBed.inject(ModelSerializerService) instanceof ModelSerializer).toBeTruthy();
expect(TestBed.inject(ArraySerializerService) instanceof ArraySerializer).toBeTruthy();
expect(TestBed.inject(ObjectSerializerService) instanceof ObjectSerializer).toBeTruthy();
expect(TestBed.inject(PrimitiveSerializerService) instanceof PrimitiveSerializer).toBeTruthy();
expect(TestBed.inject(SerializationManagerService) instanceof SerializationManager).toBeTruthy();
expect(TestBed.inject(VariableSerializerService) instanceof VariableSerializer).toBeTruthy();
expect(TestBed.inject(EditorLibraryService) instanceof EditorLibrary).toBeTruthy();
expect(TestBed.inject(EditorApiFactoryService) instanceof EditorApiFactory).toBeTruthy();
expect(TestBed.inject(ModelPropertyTypeService) instanceof ModelPropertyType).toBeTruthy();
expect(TestBed.inject(DataRefreshEventService) instanceof DataRefreshEvent).toBeTruthy();
expect(TestBed.inject(TimeRangeChangedEventService) instanceof TimeRangeChangedEvent).toBeTruthy();
expect(TestBed.inject(TimeRangeManagerService) instanceof TimeRangeManager).toBeTruthy();
});

test('support model decorators', () => {
@Model({ type: 'test-model-decorator' })
class TestModelDecoratorClass {}

expect(
(TestBed.get(ModelLibraryService) as ModelLibrary).lookupModelMetadata(TestModelDecoratorClass)
(TestBed.inject(ModelLibraryService) as ModelLibrary).lookupModelMetadata(TestModelDecoratorClass)
).toMatchObject({
type: 'test-model-decorator'
});
@@ -129,7 +129,7 @@ describe('Injectable wrappers', () => {
class TestLateModelDecoratorClass {}

expect(
(TestBed.get(ModelLibraryService) as ModelLibrary).lookupModelMetadata(TestLateModelDecoratorClass)
(TestBed.inject(ModelLibraryService) as ModelLibrary).lookupModelMetadata(TestLateModelDecoratorClass)
).toMatchObject({
type: 'test-late-model-decorator'
});
@@ -141,7 +141,7 @@ describe('Injectable wrappers', () => {
@Renderer({ modelClass: modelClass })
class TestRendererDecoratorClass {}

expect((TestBed.get(RendererLibraryService) as RendererLibrary).lookupRenderer(modelClass)).toBe(
expect((TestBed.inject(RendererLibraryService) as RendererLibrary).lookupRenderer(modelClass)).toBe(
TestRendererDecoratorClass
);

@@ -151,7 +151,7 @@ describe('Injectable wrappers', () => {
@Renderer({ modelClass: secondModelClass })
class TestSecondRendererDecoratorClass {}

expect((TestBed.get(RendererLibraryService) as RendererLibrary).lookupRenderer(secondModelClass)).toBe(
expect((TestBed.inject(RendererLibraryService) as RendererLibrary).lookupRenderer(secondModelClass)).toBe(
TestSecondRendererDecoratorClass
);
});
@@ -180,7 +180,7 @@ describe('Injectable wrappers', () => {
public booleanProp?: boolean;
}

expect((TestBed.get(EditorLibraryService) as EditorLibrary).getEditorData(TestModelClass)).toMatchObject({
expect((TestBed.inject(EditorLibraryService) as EditorLibrary).getEditorData(TestModelClass)).toMatchObject({
title: 'Test Model',
subeditors: [
{
@@ -193,7 +193,7 @@ describe('Injectable wrappers', () => {
@ModelPropertyEditor({ propertyType: BOOLEAN_PROPERTY.type })
class LateTestEditorClass {}

expect((TestBed.get(EditorLibraryService) as EditorLibrary).getEditorData(TestModelClass)).toMatchObject({
expect((TestBed.inject(EditorLibraryService) as EditorLibrary).getEditorData(TestModelClass)).toMatchObject({
title: 'Test Model',
subeditors: [
{
Original file line number Diff line number Diff line change
@@ -15,11 +15,11 @@ describe('Model event installer service', () => {
TestBed.configureTestingModule({
providers: [{ provide: eventKeyInjectionToken, useValue: eventKey }]
});
modelEventInstallerService = TestBed.get(ModelEventInstallerService);
modelEventInstallerService = TestBed.inject(ModelEventInstallerService);
});

test('should support injecting event keys', () => {
const dashEventManagerService: DashboardEventManagerService = TestBed.get(DashboardEventManagerService);
const dashEventManagerService: DashboardEventManagerService = TestBed.inject(DashboardEventManagerService);
dashEventManagerService.publishEvent = jest.fn();

class TestModelClass {
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import { ModelPropertyType } from '@hypertrace/hyperdash';
import { DeserializationManagerService } from './deserialization/deserialization-manager.service';
import { ModelManagerService } from './model-manager.service';
/**
* Injectable implementation of `RendererLModelPropertyTypeibrary`
* Injectable implementation of `ModelPropertyType`
*/
@Injectable({
providedIn: 'root'
117 changes: 115 additions & 2 deletions projects/hyperdash-angular/src/module/dashboard-core.module.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { Injectable } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ModelPropertyTypeRegistrationInformation } from '@hypertrace/hyperdash';
import {
ARRAY_PROPERTY,
BOOLEAN_PROPERTY,
ModelPropertyTypeRegistrationInformation,
NUMBER_PROPERTY,
PLAIN_OBJECT_PROPERTY,
STRING_PROPERTY
} from '@hypertrace/hyperdash';
import { DefaultConfigurationService } from '../configuration/default-configuration.service';
import { DeserializationManagerService } from '../injectable-wrappers/deserialization/deserialization-manager.service';
import { ModelLibraryService } from '../injectable-wrappers/model-library.service';
import { ModelPropertyTypeService } from '../injectable-wrappers/model-property-type.service';
import { ModelPropertyValidatorService } from '../injectable-wrappers/model-property-validator.service';
import { VariableManagerService } from '../injectable-wrappers/variable-manager.service';
import { DashboardCoreModule, MODEL_PROPERTY_TYPES } from './dashboard-core.module';

describe('Dashboard core module', () => {
@@ -28,6 +41,106 @@ describe('Dashboard core module', () => {
]
});

expect(TestBed.get(MODEL_PROPERTY_TYPES)).toContainEqual([testPropType, testPropType2, PropertyTypeProvider]);
expect(TestBed.inject(MODEL_PROPERTY_TYPES)).toContainEqual([testPropType, testPropType2, PropertyTypeProvider]);
});

test('correctly configures deserialization', () => {
TestBed.configureTestingModule({
imports: [DashboardCoreModule]
});

const deserializationManager = TestBed.inject(DeserializationManagerService);
const modelLibrary = TestBed.inject(ModelLibraryService);

TestBed.inject(ModelPropertyValidatorService).setStrictSchema(false);
const testModel = class ModelClass {
public constructor(public prop: unknown) {}
};

modelLibrary.registerModelClass(testModel, { type: 'test-model' });
modelLibrary.registerModelProperty(testModel, 'prop', {
type: BOOLEAN_PROPERTY.type,
key: 'boolProp'
});
modelLibrary.registerModelProperty(testModel, 'prop', {
type: ARRAY_PROPERTY.type,
key: 'arrayProp'
});
modelLibrary.registerModelProperty(testModel, 'prop', {
type: PLAIN_OBJECT_PROPERTY.type,
key: 'objectProp'
});
modelLibrary.registerModelProperty(testModel, 'prop', {
type: ModelPropertyTypeService.TYPE,
key: 'modelProp'
});
modelLibrary.registerModelProperty(testModel, 'prop', {
type: STRING_PROPERTY.type,
key: 'stringProp'
});

TestBed.inject(DefaultConfigurationService).configure();
expect(deserializationManager.deserialize({ type: 'test-model', boolProp: false })).toEqual(new testModel(false));
expect(deserializationManager.deserialize({ type: 'test-model', arrayProp: [false] })).toEqual(
new testModel([false])
);
expect(deserializationManager.deserialize({ type: 'test-model', objectProp: { nested: false } })).toEqual(
new testModel({ nested: false })
);

expect(
deserializationManager.deserialize({
type: 'test-model',
modelProp: {
type: 'test-model',
stringProp: 'two models'
}
})
).toEqual(new testModel(new testModel('two models')));

expect(
deserializationManager.deserialize({
type: 'test-model',
objectProp: {
nested: {
type: 'test-model',
stringProp: 'object sandwich'
}
}
})
).toEqual(new testModel({ nested: new testModel('object sandwich') }));
});

test('correctly configures deserialization for variables', () => {
TestBed.configureTestingModule({
imports: [DashboardCoreModule]
});
const deserializationManager = TestBed.inject(DeserializationManagerService);
const modelLibrary = TestBed.inject(ModelLibraryService);

const testModel = class ModelClass {
public constructor(public prop?: number) {}
};

modelLibrary.registerModelClass(testModel, { type: 'test-model' });
modelLibrary.registerModelProperty(testModel, 'prop', {
type: NUMBER_PROPERTY.type,
key: 'prop',
required: false
});

TestBed.inject(DefaultConfigurationService).configure();

const deserializedModel = deserializationManager.deserialize<object>({
type: 'test-model',
// tslint:disable-next-line:no-invalid-template-strings
prop: '${test}'
});

expect(deserializedModel).toEqual(new testModel());

TestBed.inject(VariableManagerService).set('test', 42, deserializedModel);

expect(deserializedModel).toEqual(new testModel(42));
});
});
53 changes: 41 additions & 12 deletions projects/hyperdash-angular/src/module/dashboard-core.module.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import { InjectionToken, ModuleWithProviders, NgModule, Type } from '@angular/core';
import { AbstractType, InjectionToken, ModuleWithProviders, NgModule, Type } from '@angular/core';
import {
ARRAY_PROPERTY,
BOOLEAN_PROPERTY,
Deserializer,
ModelPropertyTypeRegistrationInformation,
NUMBER_PROPERTY,
PLAIN_OBJECT_PROPERTY,
STRING_PROPERTY,
UNKNOWN_PROPERTY
} from '@hypertrace/hyperdash';
import { ArrayDeserializerService } from '../injectable-wrappers/deserialization/array-deserializer.service';
import { ModelDeserializerService } from '../injectable-wrappers/deserialization/model-deserializer.service';
import { ObjectDeserializerService } from '../injectable-wrappers/deserialization/object-deserializer.service';
import { PrimitiveDeserializerService } from '../injectable-wrappers/deserialization/primitive-deserializer.service';
import { VariableDeserializerService } from '../injectable-wrappers/deserialization/variable-deserializer.service';
import { ModelPropertyTypeService } from '../injectable-wrappers/model-property-type.service';
import { DashboardModelDirective } from '../rendering/dashboard-model.directive';
import { DashboardComponent } from '../rendering/dashboard.component';
import { ThemePropertyPipe } from '../rendering/theme-property.pipe';

export const MODEL_PROPERTY_TYPES =
// tslint:disable-next-line: max-line-length
new InjectionToken<(ModelPropertyTypeRegistrationInformation | Type<ModelPropertyTypeRegistrationInformation>)[][]>(
'MODEL_PROPERTY_TYPES'
);

export const MODEL_PROPERTY_TYPES = new InjectionToken<
(ModelPropertyTypeRegistrationInformation | Type<ModelPropertyTypeRegistrationInformation>)[][]
>('MODEL_PROPERTY_TYPES');
export const DASHBOARD_DESERIALIZERS = new InjectionToken<(Type<Deserializer> | AbstractType<Deserializer>)[]>(
'DASHBOARD_DESERIALIZERS'
);
const RETAINED_REFERENCES = new InjectionToken<Type<object>[][]>('RETAINED_REFERENCES');

/**
@@ -40,6 +46,17 @@ const RETAINED_REFERENCES = new InjectionToken<Type<object>[][]>('RETAINED_REFER
ModelPropertyTypeService
],
multi: true
},
{
provide: DASHBOARD_DESERIALIZERS,
useValue: [
ObjectDeserializerService,
ArrayDeserializerService,
ModelDeserializerService,
PrimitiveDeserializerService,
VariableDeserializerService
],
multi: true
}
]
})
@@ -58,14 +75,17 @@ export class DashboardCoreModule {
{
provide: MODEL_PROPERTY_TYPES,
multi: true,
// tslint:disable-next-line: strict-boolean-expressions
useValue: metadata.propertyTypes || []
useValue: metadata.propertyTypes ?? []
},
{
provide: RETAINED_REFERENCES,
multi: true,
// tslint:disable-next-line: strict-boolean-expressions
useValue: [...(metadata.models || []), ...(metadata.renderers || []), ...(metadata.editors || [])]
useValue: [...(metadata.models ?? []), ...(metadata.renderers ?? []), ...(metadata.editors ?? [])]
},
{
provide: DASHBOARD_DESERIALIZERS,
multi: true,
useValue: metadata.deserializers ?? []
}
]
};
@@ -97,5 +117,14 @@ export interface DashboardMetadata {
/**
* New property types to be registered, or injectable classes that implement a property type
*/
propertyTypes?: (Type<ModelPropertyTypeRegistrationInformation> | ModelPropertyTypeRegistrationInformation)[];
propertyTypes?: (
| AbstractType<ModelPropertyTypeRegistrationInformation>
| Type<ModelPropertyTypeRegistrationInformation>
| ModelPropertyTypeRegistrationInformation
)[];

/**
* New deserializer classes to be registered
*/
deserializers?: (AbstractType<Deserializer> | Type<Deserializer>)[];
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { Component, Input } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DashboardModelDirective } from './dashboard-model.directive';
import { DashboardRendererService } from './dashboard-renderer.service';

describe('DashboardModelDirective', () => {
let dashboardRendererService: DashboardRendererService;
let host: ComponentFixture<HostComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [{ provide: DashboardRendererService, useValue: {} }],
declarations: [HostComponent, DashboardModelDirective]
}).compileComponents();
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
providers: [{ provide: DashboardRendererService, useValue: {} }],
declarations: [HostComponent, DashboardModelDirective]
}).compileComponents();

dashboardRendererService = TestBed.get(DashboardRendererService);
dashboardRendererService.renderInViewContainer = jest.fn();
dashboardRendererService = TestBed.inject(DashboardRendererService);
dashboardRendererService.renderInViewContainer = jest.fn();

host = TestBed.createComponent(HostComponent);
}));
host = TestBed.createComponent(HostComponent);
})
);

test('passes initial value to renderer service', () => {
expect(dashboardRendererService.renderInViewContainer).not.toHaveBeenCalled();
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, Inject, ViewChild, ViewContainerRef } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ModelApi } from '@hypertrace/hyperdash';
import { ReplaySubject } from 'rxjs';
@@ -21,24 +21,26 @@ describe('Dashboard Renderer Service', () => {
return model;
};

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [HostComponent],
imports: [moduleWithEntryComponents(RendererComponent)]
}).compileComponents();

dashboardRendererService = TestBed.get(DashboardRendererService);
modelManager = TestBed.get(ModelManagerService) as ModelManagerService;
modelManager.registerModelApiBuilder({
matches: () => true,
// tslint:disable-next-line: no-object-literal-type-assertion
build: () => ({} as ModelApi)
});
const rendererLibrary = TestBed.get(RendererLibraryService);
rendererLibrary.lookupRenderer = jest.fn().mockReturnValue(RendererComponent);
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [HostComponent],
imports: [moduleWithEntryComponents(RendererComponent)]
}).compileComponents();

dashboardRendererService = TestBed.inject(DashboardRendererService);
modelManager = TestBed.inject(ModelManagerService);
modelManager.registerModelApiBuilder({
matches: () => true,
// tslint:disable-next-line: no-object-literal-type-assertion
build: () => ({} as ModelApi)
});
const rendererLibrary = TestBed.inject(RendererLibraryService);
rendererLibrary.lookupRenderer = jest.fn().mockReturnValue(RendererComponent);

host = TestBed.createComponent(HostComponent);
}));
host = TestBed.createComponent(HostComponent);
})
);

test('can render', () => {
dashboardRendererService.renderInViewContainer(createModel('Renderer'), host.componentInstance.viewContainerRef);
Original file line number Diff line number Diff line change
@@ -53,7 +53,7 @@ describe('Dashboard Component', () => {
declarations: [HostComponent, DashboardComponent, mockDashDirective]
});

dashboardManagerService = TestBed.get(DashboardManagerService);
dashboardManagerService = TestBed.inject(DashboardManagerService);
dashboardManagerService.create = jest.fn().mockImplementation(val => ({ root: val, destroy: jest.fn() }));

host = TestBed.createComponent(HostComponent);
@@ -130,7 +130,7 @@ describe('Dashboard Component', () => {
expect(componentChangeDetector.markForCheck).not.toHaveBeenCalled();

// Trigger a model change event
const modelChanged = TestBed.get(ModelChangedEventService) as ModelChangedEventService;
const modelChanged = TestBed.inject(ModelChangedEventService);
modelChanged.publishChange(host.componentInstance.dashboard!.root);

expect(componentChangeDetector.markForCheck).toHaveBeenCalled();
36 changes: 19 additions & 17 deletions src/example-dash/example-dash.component.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { async, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import {
DashboardCoreModule,
@@ -12,22 +12,24 @@ import { ExampleDashModule } from './example-dash.module';
import { GraphQlDataSourceModule } from './graphql-data-source.module';

describe('example dash component', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ExampleDashComponent],
imports: [
ExampleDashModule,
DashboardCoreModule,
DashboardEditorModule,
FormsModule,
GraphQlDataSourceModule,
HttpClientTestingModule
],
providers: [{ provide: LoggerService, useValue: { warn: jest.fn() } }]
}).compileComponents();

(TestBed.get(DefaultConfigurationService) as DefaultConfigurationService).configure();
}));
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ExampleDashComponent],
imports: [
ExampleDashModule,
DashboardCoreModule,
DashboardEditorModule,
FormsModule,
GraphQlDataSourceModule,
HttpClientTestingModule
],
providers: [{ provide: LoggerService, useValue: { warn: jest.fn() } }]
}).compileComponents();

TestBed.inject(DefaultConfigurationService).configure();
})
);

test('can detect clicks inside the dashboard', () => {
const fixture = TestBed.createComponent(ExampleDashComponent);
9 changes: 0 additions & 9 deletions src/tsconfig.app.json

This file was deleted.

9 changes: 9 additions & 0 deletions tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts", "src/polyfills.ts"],
"include": ["src/**/*.d.ts"]
}
5 changes: 4 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -8,7 +8,10 @@
"files": [],
"references": [
{
"path": "./src/tsconfig.app.json"
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./projects/hyperdash-angular/tsconfig.lib.json"
6 changes: 3 additions & 3 deletions src/tsconfig.spec.json → tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"extends": "../tsconfig.base.json",
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "../out-tsc/spec",
"outDir": "./out-tsc/spec",
"types": ["jest", "webpack-env"],
"module": "commonjs"
},
"files": ["polyfills.ts"],
"files": ["src/polyfills.ts"],
"include": ["**/*.spec.ts", "**/*.test.ts"]
}