Skip to content
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

chore: convert effect serializer to TypeScript #295

Merged
Merged
Show file tree
Hide file tree
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
70 changes: 50 additions & 20 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,11 +72,11 @@ 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,
Expand Down Expand Up @@ -89,17 +108,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