Skip to content

Commit

Permalink
chore: convert effect serializer to TypeScript
Browse files Browse the repository at this point in the history
  • Loading branch information
pvlugter committed Apr 8, 2022
1 parent bca580c commit d2fb6b7
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 80 deletions.
79 changes: 53 additions & 26 deletions sdk/src/effect-serializer.js → sdk/src/effect-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,33 @@
* limitations under the License.
*/

const AnySupport = require('./protobuf-any');
const util = require('util');
import { ServiceMap } from './kalix';
import protobuf from 'protobufjs';
import AnySupport from './protobuf-any';
import util from 'util';
import grpc from '@grpc/grpc-js';
import { Effect } from './effect';
import { Metadata } from './metadata';

module.exports = class EffectSerializer {
constructor(allComponents) {
this.allComponents = allComponents;
class EffectSerializer {
private services: ServiceMap;

constructor(services: ServiceMap) {
this.services = services;
}

serializeEffect(method, message, metadata) {
let serviceName, commandName;
serializeEffect(
method:
| grpc.MethodDefinition<any, any>
| protobuf.Method
| protobuf.ReflectionObject
| null,
message: { [key: string]: any },
metadata?: Metadata,
): Effect {
let serviceName: string, commandName: string;
// We support either the grpc method, or a protobufjs method being passed
if (typeof method.path === 'string') {
if (method && 'path' in method && typeof method.path === 'string') {
const r = new RegExp('^/([^/]+)/([^/]+)$').exec(method.path);
if (r == null) {
throw new Error(
Expand All @@ -38,12 +53,16 @@ module.exports = class EffectSerializer {
}
serviceName = r[1];
commandName = r[2];
} else if (method.type === 'rpc') {
} else if (method && 'type' in method && method.type === 'rpc') {
serviceName = this.fullName(method.parent);
commandName = method.name;
} else {
throw new Error(
'Method must either be a gRPC MethodDefinition or a protobufjs Method',
);
}

const service = this.allComponents[serviceName];
const service = this.services[serviceName];

if (service !== undefined) {
const command = service.methods[commandName];
Expand All @@ -53,22 +72,19 @@ module.exports = class EffectSerializer {
}

const payload = AnySupport.serialize(
command.resolvedRequestType.create(message),
command.resolvedRequestType!.create(message),
false,
false,
);
const effect = {
const effect: Effect = {
serviceName: serviceName,
commandName: commandName,
payload: payload,
metadata: {
entries: metadata?.entries ?? [],
},
};

if (metadata && metadata.entries) {
effect.metadata = {
entries: metadata.entries,
};
}

return effect;
} else {
throw new Error(
Expand All @@ -89,17 +105,28 @@ module.exports = class EffectSerializer {
}
}

fullName(item) {
if (item.parent && item.parent.name !== '') {
fullName(item: protobuf.NamespaceBase | null): string {
if (item?.parent && item.parent.name !== '') {
return this.fullName(item.parent) + '.' + item.name;
} else {
return item.name;
return item?.name ?? '';
}
}

serializeSideEffect(method, message, synchronous, metadata) {
const msg = this.serializeEffect(method, message, metadata);
msg.synchronous = typeof synchronous === 'boolean' ? synchronous : false;
return msg;
serializeSideEffect(
method:
| grpc.MethodDefinition<any, any>
| protobuf.Method
| protobuf.ReflectionObject
| null,
message: { [key: string]: any },
synchronous?: boolean,
metadata?: Metadata,
): Effect {
const effect = this.serializeEffect(method, message, metadata);
effect.synchronous = typeof synchronous === 'boolean' ? synchronous : false;
return effect;
}
};
}

export = EffectSerializer;
30 changes: 30 additions & 0 deletions sdk/src/effect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2021 Lightbend Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { MetadataEntry } from './metadata';
import * as protobufHelper from './protobuf-helper';

type Any = protobufHelper.moduleRoot.google.protobuf.Any;

export interface Effect {
serviceName: string;
commandName: string;
payload: Any;
metadata: {
entries: MetadataEntry[];
};
synchronous?: boolean;
}
14 changes: 9 additions & 5 deletions sdk/src/kalix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,17 @@ export interface EntityOptions {
export interface Component {
serviceName: string;
desc?: string | string[];
service?: any;
service?: protobuf.Service;
options: ComponentOptions | EntityOptions;
grpc?: grpc.GrpcObject;
componentType: () => string;
register?: (components: any) => ComponentService;
}

export interface ServiceMap {
[key: string]: protobuf.Service;
}

class DocLink {
private specificCodes: Map<string, string> = new Map([
['AS-00112', 'javascript/views.html#changing'],
Expand Down Expand Up @@ -312,16 +316,16 @@ export class Kalix {
}
}

const allComponentsMap: any = {};
const serviceMap: ServiceMap = {};
this.components.forEach((component: Component) => {
allComponentsMap[component.serviceName ?? 'undefined'] =
component.service;
if (component.service)
serviceMap[component.serviceName] = component.service;
});

const componentTypes: any = {};
this.components.forEach((component: Component) => {
if (component.register) {
const componentServices = component.register(allComponentsMap);
const componentServices = component.register(serviceMap);
componentTypes[componentServices.componentType()] = componentServices;
}
});
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { JwtClaims } from './jwt-claims';
type MetadataValue = string | Buffer;

// Using an interface for compatibility with legacy JS code
interface MetadataEntry {
export interface MetadataEntry {
readonly key: string;
readonly bytesValue: Buffer | undefined;
readonly stringValue: string | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,150 +14,117 @@
* limitations under the License.
*/

const should = require('chai').should();
const EffectSerializer = require('../src/effect-serializer');

const protobuf = require('protobufjs');
const path = require('path');
import { should as chaiShould } from 'chai';
import EffectSerializer from '../src/effect-serializer';
import protobuf from 'protobufjs';
import path from 'path';

const should = chaiShould();
const root = new protobuf.Root();
root.loadSync(path.join(__dirname, 'example.proto'));
const In = root.lookupType('com.example.In');
const exampleService = root.lookupService('com.example.ExampleService');
const exampleServiceTwo = root.lookupService('com.example.ExampleServiceTwo');
const exampleServiceGenerated = require('./proto/example_grpc_pb');
import exampleServiceGenerated from './proto/example_grpc_pb';

describe('Effect Serializer', () => {
it('should throw error if the service is not registered', () => {
// Arrange
const es = new EffectSerializer();
const es = new EffectSerializer({});

// Act
const res = () =>
es.serializeEffect(exampleService.methods.DoSomething, {}, {});
es.serializeEffect(exampleService.methods.DoSomething, {});

// Assert
should.throw(() => res(), Error);
});

it('should throw error if the method is not part of this service', () => {
// Arrange
const es = new EffectSerializer();
const es = new EffectSerializer({});

// Act
const res = () =>
es.serializeEffect(exampleServiceTwo.methods.DoSomethingOne, {}, {});
es.serializeEffect(exampleServiceTwo.methods.DoSomethingOne, {});

// Assert
should.throw(() => res(), Error);
});

it('should serialize successfully', () => {
// Arrange
const es = new EffectSerializer({
'com.example.ExampleService': exampleService,
});
const msg = In.create({ field: 'foo' });

// Act
const res = es.serializeEffect(exampleService.methods.DoSomething, msg, {});
const res = es.serializeEffect(exampleService.methods.DoSomething, msg);

// Assert
res.serviceName.should.eq('com.example.ExampleService');
res.commandName.should.eq('DoSomething');
res.payload.type_url.should.eq('type.googleapis.com/com.example.In');
});

it('should serialize successfully', () => {
// Arrange
const es = new EffectSerializer({
'com.example.ExampleService': exampleService,
});
const msg = In.create({ field: 'foo' });

// Act
const res = es.serializeEffect(
exampleService.methods.DoSomething.resolve(),
msg,
{},
);
exampleService.methods.DoSomething.resolve();
const res = es.serializeEffect(exampleService.methods.DoSomething, msg);

// Assert
res.serviceName.should.eq('com.example.ExampleService');
res.commandName.should.eq('DoSomething');
res.payload.type_url.should.eq('type.googleapis.com/com.example.In');
});

it('should serialize successfully unresolved methods', () => {
// Arrange
const es = new EffectSerializer({
'com.example.ExampleService': exampleService,
});
const msg = In.create({ field: 'foo' });

// Act
const res = es.serializeEffect(exampleService.methods.DoSomething, msg, {});
const res = es.serializeEffect(exampleService.methods.DoSomething, msg);

// Assert
res.serviceName.should.eq('com.example.ExampleService');
res.commandName.should.eq('DoSomething');
res.payload.type_url.should.eq('type.googleapis.com/com.example.In');
});

it('should serialize successfully using lookup', () => {
// Arrange
const es = new EffectSerializer({
'com.example.ExampleService': exampleService,
});
const msg = In.create({ field: 'foo' });

// Act
const res = es.serializeEffect(
exampleService.lookup('DoSomething'),
msg,
{},
);
const res = es.serializeEffect(exampleService.lookup('DoSomething'), msg);

// Assert
res.serviceName.should.eq('com.example.ExampleService');
res.commandName.should.eq('DoSomething');
res.payload.type_url.should.eq('type.googleapis.com/com.example.In');
});

it('should reject methods on the incorrect service using the generated gRPC definition', () => {
// Arrange
const es = new EffectSerializer({
'com.example.ExampleService': exampleService,
});
const msg = In.create({ field: 'foo' });

// Act
const res = () =>
es.serializeEffect(
exampleServiceGenerated.ExampleServiceTwoService.doSomethingOne,
msg,
{},
);

// Assert
should.throw(() => res(), Error);
});

it('should serialize successfully methods using the generated gRPC definition', () => {
// Arrange
const es = new EffectSerializer({
'com.example.ExampleService': exampleService,
});
const msg = In.create({ field: 'foo' });

// Act
const res = es.serializeEffect(
exampleServiceGenerated.ExampleServiceService.doSomething,
msg,
{},
);

// Assert
res.serviceName.should.eq('com.example.ExampleService');
res.commandName.should.eq('DoSomething');
res.payload.type_url.should.eq('type.googleapis.com/com.example.In');
Expand Down

0 comments on commit d2fb6b7

Please sign in to comment.