Skip to content
This repository has been archived by the owner on Jul 20, 2021. It is now read-only.

Commit

Permalink
feat(initial): add initial mappers and data mapper chain
Browse files Browse the repository at this point in the history
  • Loading branch information
Per Kristian Kummermo committed May 2, 2018
1 parent e4d96b9 commit 0853dab
Show file tree
Hide file tree
Showing 17 changed files with 9,343 additions and 0 deletions.
8,090 changes: 8,090 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@exploratoryengineering/data-mapper-chain",
"version": "0.1.0",
"description": "Simple data mapper library meant to be run in browser to ease data transformation for IoT devices in JS",
"main": "index.ts",
"scripts": {
"commitmsg": "validate-commit-msg",
"release": "standard-version --silent",
"release:patch": "npm run release -- --release-as patch",
"release:minor": "npm run release -- --release-as minor",
"release:major": "npm run release -- --release-as major",
"test": "npm run test:unit && npm run test:tslint",
"test:unit": "del-cli test/coverage-jest && jest --config jest.config.json --forceExit",
"test:tslint": "tslint \"src/**/*.ts\" -c tslint.json"
},
"keywords": [
"iot",
"data",
"transform"
],
"author": "Per Kristian Kummermo",
"license": "Apache-2.0",
"devDependencies": {
"@telenorfrontend/tslint-config-telenor": "^0.1.1",
"@types/jest": "^22.2.3",
"del-cli": "^1.1.0",
"husky": "^0.14.3",
"jest": "^22.4.3",
"jest-cli": "^22.4.3",
"jest-junit-reporter": "^1.1.0",
"standard-version": "^4.3.0",
"ts-jest": "^22.4.4",
"tslint": "^5.9.1",
"typescript": "^2.8.3",
"validate-commit-msg": "^2.14.0"
}
}
190 changes: 190 additions & 0 deletions src/DataMapperChain.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import DataMapperChain, { AVAILABLE_MAPPERS_TYPES } from "./DataMapperChain";

class MapperMock implements IMapper {
initParams: any = {};

constructor(params = {}) {
this.initParams = params;
}

transform(value: IDataValue): IDataValue {
return {
...value, ...{
name: "MOCK WAS HERE",
},
};
}

config(): IMapperConfig {
return {
ident: "MOCK",
params: {
p1: 1,
p2: "2",
},
};
}
}

describe("Data mapper chain", () => {
let dataMapperChain: DataMapperChain;

beforeEach(() => {
dataMapperChain = new DataMapperChain();
dataMapperChain.addNewMapperType({
id: "MOCK",
value: "My mock mapper",
entity: MapperMock,
});
});

describe("Initializing", () => {
it("should initially have 0 mappers", () => {
expect(dataMapperChain.mappers.length).toBe(0);
});

it("should allow for init with mappers", () => {
const mappers: IMapper[] = [];

mappers.push(new MapperMock());

dataMapperChain = new DataMapperChain({
mappers: mappers,
});

expect(dataMapperChain.mappers.length).toBe(1);
});

it("should be correctly named upon init", () => {
dataMapperChain = new DataMapperChain({
name: "test",
});

expect(dataMapperChain.name).toBe("test");
});
});

describe("Config de-/serialization", () => {
it("should correctly serialize when no mappers", () => {
dataMapperChain.name = "Test";
dataMapperChain.mappers = [];

const serializedConfig = dataMapperChain.serializeConfig();
expect(serializedConfig).toBe("{\"name\":\"Test\",\"mappers\":[]}");
});

it("should correctly serialize with mapper", () => {
dataMapperChain.addMapper(new MapperMock());

const serializedConfig = dataMapperChain.serializeConfig();
expect(serializedConfig).toBe("{\"name\":\"\",\"mappers\":[{\"ident\":\"MOCK\",\"params\":{\"p1\":1,\"p2\":\"2\"}}]}");
});

it("should correctly deserialize with no mappers", () => {
const serializedConfig = "{\"name\":\"Test\",\"mappers\":[]}";

dataMapperChain.loadConfig(serializedConfig);

expect(dataMapperChain.name).toBe("Test");
});

it("should correctly deserialize with mapper and corresponding config", () => {
const serializedConfig = "{\"name\":\"Test\",\"mappers\":[{\"ident\":\"MOCK\",\"params\":{\"p1\":1,\"p2\":\"2\"}}]}";

dataMapperChain.loadConfig(serializedConfig);

expect(dataMapperChain.name).toBe("Test");
expect(dataMapperChain.mappers.length).toBe(1);
expect((dataMapperChain.mappers[0] as MapperMock).initParams).toEqual({
p1: 1,
p2: "2",
});
});
});

describe("Mapper configuration", () => {
it("should correctly add new mapper type", () => {
dataMapperChain.addNewMapperType({
id: "MYNEWMOCKMAPPER",
value: "My mock mapper",
entity: MapperMock,
});

expect(dataMapperChain.findMapperTypeById("MYNEWMOCKMAPPER")).not.toBe(undefined);
});

it("should correctly return mapper initiated with params if type is present", () => {
const mapperRes = dataMapperChain.getMapperByConfig({
ident: "MOCK",
params: "params",
});

expect(mapperRes).not.toBe(false);
expect((mapperRes as MapperMock).initParams).toBe("params");
});

it("should correctly return false when no mapper type found", () => {
const mapperRes = dataMapperChain.getMapperByConfig({
ident: "NONEXISTANT",
params: {},
});

expect(mapperRes).toBe(false);
});
});

describe("Data transformation", () => {
it("should work as intended with no mappers", () => {
const inputObj = {
name: "name",
value: "babe",
};

const transformRes = dataMapperChain.mapData(inputObj);

expect(transformRes).toEqual(inputObj);
});

it("should allow for empty input in map data", () => {
const transformRes = dataMapperChain.mapData();
expect(transformRes).toEqual({
name: "Unnamed data",
value: "",
});
});

it("should allow for partial input in map data", () => {
const transformRes = dataMapperChain.mapData({
value: "my value",
});
expect(transformRes).toEqual({
name: "Unnamed data",
value: "my value",
});
});

it("should correctly run transform on added mappers", () => {
const inputObj = {
name: "name",
value: "babe",
};
dataMapperChain.addMapper(new MapperMock());
const transformRes = dataMapperChain.mapData(inputObj);

expect(transformRes).toEqual({
name: "MOCK WAS HERE",
value: "babe",
});
});
});

describe("Invalid input", () => {
it("should correctly ignore serialized unknown mappers", () => {
const serializedConfig = "{\"name\":\"Test\",\"mappers\":[{\"ident\":\"UNKNOWNMAPPER\",\"params\":{\"p1\":1,\"p2\":\"2\"}}]}";
dataMapperChain.loadConfig(serializedConfig);

expect(dataMapperChain.name).toBe("Test");
expect(dataMapperChain.mappers.length).toBe(0);
});
});
});
112 changes: 112 additions & 0 deletions src/DataMapperChain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import Base64 from "./sensor-data-mapper/Base64";
import Chunk from "./sensor-data-mapper/Chunk";
import FromJSON from "./sensor-data-mapper/FromJSON";
import HexToInt from "./sensor-data-mapper/HexToInt";
import Offset from "./sensor-data-mapper/Offset";

interface MapperType {
id: string;
value: string;
entity: { new(params): IMapper };
}

export let AVAILABLE_MAPPERS_TYPES: MapperType[] = [{
id: Chunk.ident,
value: Chunk.description,
entity: Chunk,
}, {
id: HexToInt.ident,
value: HexToInt.description,
entity: HexToInt,
}, {
id: Offset.ident,
value: Offset.description,
entity: Offset,
}, {
id: Base64.ident,
value: Base64.description,
entity: Base64,
}, {
id: FromJSON.ident,
value: FromJSON.description,
entity: FromJSON,
}];

export default class DataMapperChain {
mappers: IMapper[] = [];
initialValue: IDataValue;
name: string = "";

constructor({
mappers = [],
name = "",
} = {}) {
this.name = name;
this.mappers = mappers;
}

serializeConfig() {
return JSON.stringify({
name: this.name,
mappers: this.mappers.map((mapper) => {
return mapper.config();
}),
});
}

loadConfig(configString: string) {
const parsedConfig = JSON.parse(configString);
this.name = parsedConfig.name;
const mapperConfigs: IMapperConfig[] = parsedConfig.mappers;

mapperConfigs.forEach((config) => {
const mapper = this.getMapperByConfig(config);
if (mapper) {
this.addMapper(mapper);
}
});
}

getMapperByConfig(config: IMapperConfig): IMapper | false {
const mapperType = this.findMapperTypeById(config.ident);
if (mapperType) {
return new mapperType.entity(config.params);
}

return false;
}

addMapper(mapper: IMapper) {
this.mappers.push(mapper);
}

addNewMapperType(mapperType: MapperType) {
const existingMapper = this.findMapperTypeById(mapperType.id);
if (!existingMapper) {
AVAILABLE_MAPPERS_TYPES.push(mapperType);
} else {
AVAILABLE_MAPPERS_TYPES.splice(
AVAILABLE_MAPPERS_TYPES.indexOf(existingMapper),
1,
mapperType,
);
}
}

findMapperTypeById(id: string) {
return AVAILABLE_MAPPERS_TYPES.find((mapperType) => {
return mapperType.id === id;
});
}

mapData({ name = "Unnamed data", value = "" } = {}) {
this.initialValue = {
name: name,
value: value,
};

return this.mappers.reduce((curr, mapper, idx) => {
return mapper.transform(curr);
}, this.initialValue);
}
}
52 changes: 52 additions & 0 deletions src/sensor-data-mapper/Base64.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Base64, { Base64Actions } from "./Base64";

describe("Base64 mapper", () => {
let base64: Base64;
const helloWorldBase64: string = "aGVsbG8gd29ybGQ=";
const helloWorldString: string = "hello world";

beforeEach(() => {
base64 = new Base64();
});

describe("Base64 encoding/decoding", () => {
it("should correctly encode a string given", () => {
base64.action = Base64Actions.ENCODE;
const transformRes = base64.transform({
name: "data",
value: helloWorldString,
});

expect(transformRes).toEqual({
name: "data",
value: helloWorldBase64,
});
});

it("should correctly decode a base 64 string given", () => {
base64.action = Base64Actions.DECODE;
const transformRes = base64.transform({
name: "data",
value: helloWorldBase64,
});

expect(transformRes).toEqual({
name: "data",
value: helloWorldString,
});
});
});

describe("Invalid input", () => {
it("should correctly return the given object if no value is present", () => {
const inputObj = {
name: "name",
value: "",
};

const transformRes = base64.transform(inputObj);

expect(transformRes).toBe(inputObj);
});
});
});
Loading

0 comments on commit 0853dab

Please sign in to comment.