How to resolve Enums as String values with Node grpc? - node.js

Using grpc with Node, Enums in responses to my queries are resolving as integer values. However, when I make the same queries with BloomRPC, the Enums resolve as Integer values.
Is there a parameter or option to force these Enums to be resolved as String using Node grpc?

In our project, we use enum to help us ensure the integrity of a finite set of possibilities by eliminating human error. Why should we need to remember what the string value is when we have the protocol buffer enum so handy? Thus, we use the .proto as the source of truth; that's our rule.
To do that, follow these steps, which are written for ES6+ code.
Define your gRPC/Protobuf enum in a .proto file.
// life.proto
syntax = 'proto3';
package life;
enum Choices
{
EAT = 0;
DRINK = 1;
SLEEP = 2;
CODE = 3;
SKI = 4;
}
Install #grpc/proto-loader and #grpc/grpc-js.
$ npm i -s #grpc/proto-loader #grpc/grpc-js
Import the modules that pay the bills, so to speak. Load the .proto file directly into memory (don't compile).
// myNodeApp.js
import * as grpc from '#grpc/grpc-js'
import * as protoLoader from '#grpc/proto-loader'
import path from 'path'
// these options help make definitions usable in our code
const protoOptions = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
}
// this allows us to prepare the path to the current dir
const dir = path.dirname(new URL(import.meta.url).pathname)
// this loads the .proto file
const lifeDef = protoLoader.loadSync(path.join(dir, 'life.proto'), protoOptions)
// this loads the package 'life' from the .proto file
const life = grpc.loadPackageDefinition(lifeDef).life
Take a peek at the enum Choices definition (in the same file).
// myNodeApp.js (cont'd)
console.log(life.Choices)
/* stdout */
{
format: 'Protocol Buffer 3 EnumDescriptorProto',
type: {
value: [ [Object], [Object], [Object], [Object], [Object] ],
name: 'Choices',
options: null
},
fileDescriptorProtos: [
<Buffer 0a ... 328 more bytes>
]
}
...look deeper...
console.log(life.Choices.value)
/* stdout */
{
value: [
{ name: 'EAT', number: 0, options: null },
{ name: 'DRINK', number: 1, options: null },
{ name: 'SLEEP', number: 2, options: null },
{ name: 'CODE', number: 3, options: null },
{ name: 'SKI', number: 4, options: null }
],
name: 'Choices',
options: null
}
Use the enum.
// myNodeApp.js
const myDay = { // plain JSON (or define a gRPC message, same same)
dawn: life.Choices.type.value[1].name,
morning: life.Choices.type.value[0].name,
afternoon: life.Choices.type.value[4].name,
evening: life.Choices.type.value[3].name,
night: life.Choices.type.value[2].name
}
You could write an accessor or utility function to manage the key lookup (by passing the imported grpc enum and index), like so:
export const getEnumByName = function (protoEnum, needle) {
return protoEnum.type.value.find(p => {
return p.name === needle
})
}
export const getEnumByNum = function (protoEnum, needle) {
return protoEnum.type.value.filter(p => {
return p.number = needle
})
}
export const getEnumKeys = function (protoEnum, key = 'name') {
return protoEnum.type.value.map(p => {
return p[key]
})
}
Inverting and assigning a value to a Message is what's already covered in other answers, just set the enum field to the string value using, you guessed it, the string that represents the enum name, which you access using the code above.
This is along the lines of how we do it. Clean and simple, just a touch obscure until you look "under the hood" one day.
Learn more about #grpc/proto-loader and #grpc/grpc-js. Hope this helps someone out there in the wild. :)

If you are using the #grpc/proto-loader library, you can set the option enums to the value String (not the string "String", the constructor function String). Then all enum values will be represented by their name strings.

Related

Eslint rule is running multiple times

I'm trying to write an eslint rule that enforces making sure the name property is defined on any classes that extend from other Error/Exception named classes (and fixes them).
As far as I can tell, it works in the astexplorer.net individually, but when I'm running it alongside other rules, it ends up getting ran multiple times, so the name property ends up being repeated multiple times in the resulting "fixed" file.
Is there anything in particular I can do to prevent it being run multiple times? I'm assuming what's happening is that it's inserting my name = 'ClassName';, then prettier is needing to reformat the code, which it does, but then maybe it's re-running my rule? I'm not sure.
Rule/fix code shown below. I've tried things like using *fix and yield, but that doesn't seem to help either (see commented code below, based on information in the eslint documentation)
module.exports = {
meta: {
hasSuggestions: true,
type: 'suggestion',
docs: {},
fixable: 'code',
schema: [], // no options,
},
create: function (context) {
return {
ClassDeclaration: function (node) {
const regex = /.*(Error|Exception)$/;
// If the parent/superClass is has "Error" or "Exception" in the name
if (node.superClass && regex.test(node.superClass.name)) {
let name = null;
const className = node.id.name;
// Test class object name
if (!regex.test(className)) {
context.report({
node: node,
message: 'Error extensions must end with "Error" or "Exception".',
});
}
// Find name ClassProperty
node.body.body.some(function (a) {
if (a.type === 'ClassProperty' && a.key.name === 'name') {
name = a.value.value;
return true;
}
});
// Name property is required
if (!name) {
context.report({
node: node,
message: 'Error extensions should have a descriptive name',
fix(fixer) {
return fixer.replaceTextRange(
[node.body.range[0]+1, node.body.range[0]+1],
`name = '${className}';`
);
},
// *fix(fixer) {
// name = className;
// yield fixer.replaceTextRange(
// [node.body.range[0]+1, node.body.range[0]+1],
// `name = '${className}';`
// );
//
// // extend range of the fix to the range of `node.parent`
// yield fixer.insertTextBefore(node.body, '');
// yield fixer.insertTextAfter(node.body, '');
// },
});
}
}
},
};
},
};
Turns out I had the AST Explorer set to the wrong parser, so it was showing me the wrong string name for the ClassProperty node. I should have been using PropertyDefinition instead.

Expect positive number parameter passed - jest

The test is linked to this question here which I raised (& was resolved) a few days ago. My current test is:
// Helpers
function getObjectStructure(runners) {
const backStake = runners.back.stake || expect.any(Number).toBeGreaterThan(0)
const layStake = runners.lay.stake || expect.any(Number).toBeGreaterThan(0)
return {
netProfits: {
back: expect.any(Number).toBeGreaterThan(0),
lay: expect.any(Number).toBeGreaterThan(0)
},
grossProfits: {
back: (runners.back.price - 1) * backStake,
lay: layStake
},
stakes: {
back: backStake,
lay: layStake
}
}
}
// Mock
const funcB = jest.fn(pairs => {
return pairs[0]
})
// Test
test('Should call `funcB` with correct object structure', () => {
const params = JSON.parse(fs.readFileSync(paramsPath, 'utf8'))
const { arb } = params
const result = funcA(75)
expect(result).toBeInstanceOf(Object)
expect(funcB).toHaveBeenCalledWith(
Array(3910).fill(
expect.objectContaining(
getObjectStructure(arb.runners)
)
)
)
})
The object structure of arb.runners is this:
{
"back": {
"stake": 123,
"price": 1.23
},
"lay": {
"stake": 456,
"price": 4.56
}
}
There are many different tests around this function mainly dependent upon the argument that is passed into funcA. For this example, it's 75. There's a different length of array that is passed to funcB dependent upon this parameter. However, it's now also dependent on whether the runners (back and/or lay) have existing stake properties for them. I have a beforeAll in each test which manipulates the arb in the file where I hold the params. Hence, that's why the input for the runners is different every time. An outline of what I'm trying to achieve is:
Measure the array passed into funcB is of correct length
Measure the objects within the array are of the correct structure:
2.1 If there are stakes with the runners, that's fine & the test is straight forward
2.2 If not stakes are with the runners, I need to test that; netProfits, grossProfits, & stakes properties all have positive Numbers
2.2 is the one I'm struggling with. If I try with my attempt below, the test fails with the following error:
TypeError: expect.any(...).toBeGreaterThan is not a function
As with previous question, the problem is that expect.any(Number).toBeGreaterThan(0) is incorrect because expect.any(...) is not an assertion and doesn't have matcher methods. The result of expect.any(...) is just a special value that is recognized by Jest equality matchers. It cannot be used in an expression like (runners.back.price - 1) * backStake.
If the intention is to extend equality matcher with custom behaviour, this is the case for custom matcher. Since spy matchers use built-in equality matcher anyway, spy arguments need to be asserted explicitly with custom matcher.
Otherwise additional restrictions should be asserted manually. It should be:
function getObjectStructure() {
return {
netProfits: {
back: expect.any(Number),
lay: expect.any(Number)
},
grossProfits: {
back: expect.any(Number),
lay: expect.any(Number)
},
stakes: {
back: expect.any(Number),
lay: expect.any(Number)
}
}
}
and
expect(result).toBeInstanceOf(Object)
expect(funcB).toHaveBeenCalledTimes(1);
expect(funcB).toHaveBeenCalledWith(
Array(3910).fill(
expect.objectContaining(
getObjectStructure()
)
)
)
const funcBArg = funcB.mock.calls[0][0];
const nonPositiveNetProfitsBack = funcBArg
.map(({ netProfits: { back } }, i) => [i, back])
.filter(([, val] => !(val > 0))
.map(([i, val] => `${netProfits:back:${i}:${val}`);
expect(nonPositiveNetProfitsBack).toEqual([]);
const nonPositiveNetProfitsLay = ...
Where !(val > 0) is necessary to detect NaN. Without custom matcher failed assertion won't result in meaningful message but an index and nonPositiveNetProfitsBack temporary variable name can give enough feedback to spot the problem. An array can be additionally remapped to contain meaningful values like a string and occupy less space in errors.

nodejs read .ini config file

I have a config file. It has variables stored in the following manner.
[general]
webapp=/var/www
data=/home/data
[env]
WEBAPP_DEPLOY=${general:webapp}/storage/deploy
SYSTEM_DEPLOY=${general:data}/deploy
As you can see it has 2 sections general and env. Section env uses the variables from section general.
So I want to read this file into a variable. Let's say config. Here's I want config object to look like:
{
general: {
webapp: '/var/www',
data: '/home/data'
},
env: {
WEBAPP_DEPLOY: '/var/www/storage/deploy',
SYSTEM_DEPLOY: '/home/data/deploy'
}
}
I general I am looking for a config parser for nodejs that supports string interpolation.
I would assume most ini libraries don't include the variable expansion functionality, but with lodash primitives a generic "deep object replacer" isn't too complex.
I've switched the : delimiter for . so has and get can lookup values directly.
const { get, has, isPlainObject, reduce } = require('lodash')
// Match all tokens like `${a.b}` and capture the variable path inside the parens
const re_token = /\${([\w$][\w\.$]*?)}/g
// If a string includes a token and the token exists in the object, replace it
function tokenReplace(value, key, object){
if (!value || !value.replace) return value
return value.replace(re_token, (match_string, token_path) => {
if (has(object, token_path)) return get(object, token_path)
return match_string
})
}
// Deep clone any plain objects and strings, replacing tokens
function plainObjectReplacer(node, object = node){
return reduce(node, (result, value, key) => {
result[key] = (isPlainObject(value))
? plainObjectReplacer(value, object)
: tokenReplace(value, key, object)
return result
}, {})
}
> plainObjectReplacer({ a: { b: { c: 1 }}, d: 'wat', e: '${d}${a.b.c}' })
{ a: { b: { c: 1 } }, d: 'wat', e: 'wat1' }
You'll find most config management tools (like ansible) can do this sort of variable expansion for you before app runtime, at deployment.

Iterate on string enum

I must be missing something, but I found several ways to iterate through an Enum but not on a string enum.
The following enum is given:
export enum Locales {
En = 'en',
Fr = 'fr',
De = 'de',
Es = 'es',
It = 'it',
Nl = 'nl',
No = 'no',
Tr = 'tr',
}
What I want to achieve:
I want to iterate on that string enum so that I get the values (!). What I've tried:
for (const key of Object.keys(Locales)) {
const locale: string = Locales[key];
console.log(locale); // Should print 'en', 'fr' and so on
}
The problem with above code:
Due to the strict tsconfig (which doesn't allow implicit anys) I can not compile this to javascript. Since this is not my project I can not change this tsconfig either. It highlights the key variable at Locales[key] and the error makes sense to me:
[ts] Element implicitly has an 'any' type because index expression is
not of type 'number'.
The question:
What's the proper way iterating through a string enum to get it's values with Typescript 2.6+?
As betadeveloper suggested, you can get proper type for key if you use type assertion as keyof typeof Locales. Or you can wrap it in type-safe variant of Object.keys() function like this:
export enum Locales {
En = 'en',
Fr = 'fr',
De = 'de',
Es = 'es',
It = 'it',
Nl = 'nl',
No = 'no',
Tr = 'tr',
}
function enumKeys<E>(e: E): (keyof E)[] {
return Object.keys(e) as (keyof E)[];
}
for (const key of enumKeys(Locales)) {
const locale: string = Locales[key];
console.log(locale);
}
Also, for the record, old-style for .. in loop still works:
for (let key in Locales) {
let locale = Locales[key];
console.log(locale);
}
#Artem and #betadeveloper pointed out that I can use the keyof typeof Locales type for my approach. The solution I eventually came up with looks like this:
const keys: (keyof typeof Locales)[] = <(keyof typeof Locales)[]>Object.keys(Locales);
for (const key of keys) {
const locale: string = Locales[key];
console.log(locale); // Prints 'en', 'fr' and so on
}
Lodash
Lodash is a good option to use since it is easy to use and provides a easy to understand api. From the lodash methods, forIn is the option you're looking for. To get typescript declaration files, you can install:
npm install #types/lodash
With the forIn method, you get the value and the key of Locales object.
import { forIn } from 'lodash'
enum Locales {
En = 'en',
Fr = 'fr'
// ...
}
forIn(Locales, (value, key) => console.log(value, key))
Adding to this question because I was looking for a solution.
The best solution is to use Object.values() https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values
Object.values(Locales).forEach((locale) => console.log(locale));
// or
for (const locale of Object.values(Locales)) {
console.log(locale);
}
Also consider Object.entries() to get both the keys and values of a string value enum.
const entries = Object.entries(Locales);
let LocalesArray= [];
entries.forEach((type) => {
LocalesArray.push({ key: type[0], name: type[1] });
});
Simple way to iterate over enum values and convert them into object array.

YUI, instantiable module that is not a widget?

If I want a module that is instantiable, let say, a module that handles storing preferences in a subcookies, and i want the main cookie to be configurable, but i don't want it to be a widget... what patterns should i use with YUI?
the end code should be something:
Y.use('my-pref-manager', function(Y){
var A = Y.my-pref-manager.prefStore('A"),
B = Y.my-pref-manager.prefStore('B");
// A and B are now loaded with the contents of cookies A and B, if they exist
A.set('xy', 123 );
});
So far i either found patterns that create widgets within my-module or i have to use methods directly in my-method which will be globals and lack initializers, etc.
There is a bunch of ways of doing this. You could do it using Y.Base.create, like below. The code might not be production ready, or even working properly, but hopefully it answers how you can create a module without it being a Widget.
The code below creates a module that extends Y.Base. This let us use Attributes and other cool things. Check the doc for Y.Base.
YUI.add('my-pref-manager', function (Y) {
var PrefManager = Y.Base.create('myPrefManager', Y.Base, [], {
initializer: function () {
this.after('prefsChange', this.changePref);
},
changePref: function (e) {
Y.Cookie.setSub(this.get('prefStore'), e.subAttrName, this.get(e.subAttrName));
},
setPref: function (name, val) {
this.set('prefs.'+name, val);
},
getPref: function (name) {
return this.get('prefs.'+name);
}
}, {
ATTRS: {
prefStore: {
value: null,
setter: function (val) {
return Y.Cookie.set(val, val);
}
},
prefs: {
value: {}
}
}
});
Y.namespace('My').PrefManager = PrefManager;
}, '0.0.1', {
requires: ['base', 'cookie']
});
YUI().use('my-pref-manager', function (Y) {
var A = new Y.My.PrefManager({prefStore: 'userPrefs'}),
B = new Y.My.PrefManager({prefStore: 'displayPrefs'});
A.setPref('x', 3);
A.setPref('y', 54);
B.setPref('tty', 7);
console.log(A.getPref('x')); // 3
});
Try it out: http://jsfiddle.net/B62nu/

Resources