withBreak blocks
Naveen Chawla
naveen.chwl at gmail.com
Sun Feb 18 09:02:39 UTC 2018
Kai, it's unlikely anyone will read all that code! Better to illustrate
your point with minimal code snippets
On Sun, 18 Feb 2018, 8:25 am kai zhu, <kaizhu256 at gmail.com> wrote:
> On Feb 18, 2018, at 4:52 AM, sagiv ben giat <sagiv.bengiat at gmail.com>
> wrote:
>
> > Can you provide a clear use case that can't (or shouldn't) be covered
> by what others have mentioned?
>
>
> @sagiv, if you need guidance by use-case/example, here’s a
> real-world example [1] of a validator “god” function (with 100%
> code-coverage [2]) that encapsulates most of the logic for validating
> user-inputs against the full swagger/openapi 2.0 spec [3]. attached
> screenshot showing how its used from browser-console (works just as well in
> nodejs).
>
> and yes, the code-sample makes use of break statements in:
> 1. a recursive while loop to dereference schema pointers [4]
> 2. in switch/case blocks which are conceptually similar to what you want
> to do
>
> [1] https://github.com/kaizhu256/node-swgg/blob/2018.2.1/lib.swgg.js#L4076
> [2]
> https://kaizhu256.github.io/node-swgg/build..beta..travis-ci.org/coverage.html/node-swgg/lib.swgg.js.html
> [3]
> https://github.com/OAI/OpenAPI-Specification/blob/3.0.1/versions/2.0.md
> [4]
> https://github.com/OAI/OpenAPI-Specification/blob/3.0.1/versions/2.0.md#referenceObject
>
> ```javascript
> /*
> * real-world example of swagger-validator from
> * https://github.com/kaizhu256/node-swgg/blob/2018.2.1/lib.swgg.js#L4076
> */
>
> /*jslint
> bitwise: true,
> browser: true,
> maxerr: 8,
> maxlen: 100,
> node: true,
> nomen: true,
> regexp: true,
> stupid: true
> */
>
> local.validateBySwaggerSchema = function (options) {
> /*
> * this function will validate options.data against the swagger
> options.schema
> * according to the spec defined at:
> *
> http://json-schema.org/draft-04/json-schema-validation.html#rfc.section.5
> */
> var $ref,
> circularList,
> data,
> dataReadonlyRemove2,
> ii,
> oneOf,
> schema,
> test,
> tmp;
> if (!options.schema) {
> return;
> }
> data = options.data;
> options.dataReadonlyRemove = options.dataReadonlyRemove || [{}, '',
> null];
> dataReadonlyRemove2 = options.dataReadonlyRemove[2] || {};
> schema = options.schema;
> circularList = [];
> while (true) {
> // dereference schema.schema
> while (schema.schema) {
> schema = schema.schema;
> }
> // dereference schema.oneOf
> oneOf = (data && schema.oneOf) || [];
> for (ii = 0; ii < oneOf.length; ii += 1) {
> tmp = String(oneOf[ii] && oneOf[ii].$ref)
> .replace('http://json-schema.org/draft-04/schema#', '#');
> switch (tmp + ' ' + (!local.isNullOrUndefined(data.$ref) ||
> data.in)) {
> case '#/definitions/bodyParameter body':
> case '#/definitions/formDataParameterSubSchema formData':
> case '#/definitions/headerParameterSubSchema header':
> case '#/definitions/jsonReference true':
> case '#/definitions/pathParameterSubSchema path':
> case '#/definitions/queryParameterSubSchema query':
> schema =
> local.swaggerSchemaJson.definitions[tmp.split('/')[2]];
> break;
> default:
> switch (tmp) {
> case '#/definitions/bodyParameter':
> case '#/definitions/jsonReference':
> schema = oneOf[ii ^ 1];
> break;
> }
> }
> if (!schema.oneOf) {
> break;
> }
> }
> // dereference schema.$ref
> $ref = schema && schema.$ref;
> if (!$ref) {
> break;
> }
> test = circularList.indexOf($ref) < 0;
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'schemaDeferenceCircular',
> prefix: options.prefix,
> schema: schema
> });
> circularList.push($ref);
> tmp = $ref.split('/').slice(-2);
> schema = $ref.indexOf('http://json-schema.org/draft-04/schema#/')
> === 0
> ? local.swaggerSchemaJson[tmp[0]]
> : options.swaggerJson[tmp[0]];
> schema = schema && schema[tmp[1]];
> test = schema;
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'schemaDeference',
> prefix: options.prefix,
> schema: options.schema
> });
> }
> if (options.modeDereference) {
> if (options.modeDereferenceDepth > 1) {
> schema = local.jsonCopy(schema);
> Object.keys(schema.properties || {}).forEach(function (key) {
> schema.properties[key] = local.validateBySwaggerSchema({
> // dereference property
> modeDereference: true,
> modeDereferenceDepth: options.modeDereferenceDepth - 1,
> prefix: options.prefix.concat(['properties', key]),
> schema: schema.properties[key],
> swaggerJson: options.swaggerJson
> });
> });
> }
> return schema;
> }
> // validate schema.default
> if (options.modeDefault) {
> data = schema.default;
> }
> // validate semanticRequired
> test = options.modeDefault ||
> !local.isNullOrUndefined(data) ||
> schema.required !== true ||
> schema['x-swgg-notRequired'];
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'semanticRequired',
> prefix: options.prefix,
> schema: schema
> });
> if (local.isNullOrUndefined(data)) {
> return;
> }
> // validate semanticRequiredArrayItems
> test = !options.modeSchema || local.schemaPType(data) !== 'array' ||
> (typeof local.schemaPItems(data) === 'object' &&
> local.schemaPItems(data));
> local.throwSwaggerError(!test && {
> errorType: 'semanticRequiredArrayItems',
> prefix: options.prefix,
> schema: data
> });
> // remove readOnly property
> if (schema.readOnly) {
> delete
> options.dataReadonlyRemove[0][options.dataReadonlyRemove[1]];
> }
> // optimization - validate schema.type first
> // 5.5.2. type
> // https://swagger.io/docs/specification/data-models/data-types/
> //
> https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#data-types
> switch (local.schemaPType(schema)) {
> case 'array':
> test = Array.isArray(data);
> break;
> case 'boolean':
> test = typeof data === 'boolean';
> break;
> case 'file':
> test = !options.modeSchema;
> break;
> case 'integer':
> test = Number.isFinite(data) && Math.floor(data) === data;
> switch (schema.format) {
> case 'int32':
> break;
> case 'int64':
> break;
> }
> break;
> case 'number':
> test = Number.isFinite(data);
> switch (schema.format) {
> case 'double':
> break;
> case 'float':
> break;
> }
> break;
> case 'string':
> test = typeof data === 'string' ||
> (!options.modeSchema && schema.format === 'binary');
> switch (test && !options.modeSchema && schema.format) {
> // Clarify 'byte' format #50
> // https://github.com/swagger-api/swagger-spec/issues/50
> case 'byte':
> test = !(/[^\n\r\+\/0-9\=A-Za-z]/).test(data);
> break;
> case 'date':
> case 'date-time':
> test = JSON.stringify(new Date(data)) !== 'null';
> break;
> case 'email':
> test = local.regexpEmailValidate.test(data);
> break;
> case 'json':
> test = local.tryCatchOnError(function () {
> JSON.parse(data);
> return true;
> }, local.nop);
> break;
> case 'phone':
> test = local.regexpPhoneValidate.test(data);
> break;
> }
> break;
> default:
> test = options.modeSchema || typeof data === 'object';
> break;
> }
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'itemType',
> prefix: options.prefix,
> schema: schema,
> typeof: typeof data
> });
> tmp = typeof data;
> if (tmp === 'object' && Array.isArray(data)) {
> tmp = 'array';
> }
> switch (tmp) {
> // 5.1. Validation keywords for numeric instances (number and integer)
> case 'number':
> // 5.1.1. multipleOf
> test = typeof schema.multipleOf !== 'number' || data %
> schema.multipleOf === 0;
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'numberMultipleOf',
> prefix: options.prefix,
> schema: schema
> });
> // 5.1.2. maximum and exclusiveMaximum
> test = typeof schema.maximum !== 'number' ||
> (schema.exclusiveMaximum
> ? data < schema.maximum
> : data <= schema.maximum);
> local.throwSwaggerError(!test && {
> data: data,
> errorType: schema.exclusiveMaximum
> ? 'numberExclusiveMaximum'
> : 'numberMaximum',
> prefix: options.prefix,
> schema: schema
> });
> // 5.1.3. minimum and exclusiveMinimum
> test = typeof schema.minimum !== 'number' ||
> (schema.exclusiveMinimum
> ? data > schema.minimum
> : data >= schema.minimum);
> local.throwSwaggerError(!test && {
> data: data,
> errorType: schema.exclusiveMinimum
> ? 'numberExclusiveMinimum'
> : 'numberMinimum',
> prefix: options.prefix,
> schema: schema
> });
> break;
> // 5.2. Validation keywords for strings
> case 'string':
> // 5.2.1. maxLength
> test = typeof schema.maxLength !== 'number' || data.length <=
> schema.maxLength;
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'stringMaxLength',
> prefix: options.prefix,
> schema: schema
> });
> // 5.2.2. minLength
> test = typeof schema.minLength !== 'number' || data.length >=
> schema.minLength;
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'stringMinLength',
> prefix: options.prefix,
> schema: schema
> });
> // 5.2.3. pattern
> test = !schema.pattern || new RegExp(schema.pattern).test(data);
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'stringPattern',
> prefix: options.prefix,
> schema: schema
> });
> break;
> // 5.3. Validation keywords for arrays
> case 'array':
> // 5.3.1. additionalItems and items
> // swagger disallows array items
> data.forEach(function (element, ii) {
> // recurse - schema.additionalItems and schema.items
> local.validateBySwaggerSchema({
> data: element,
> dataReadonlyRemove: [dataReadonlyRemove2, ii,
> dataReadonlyRemove2[ii]],
> modeSchema: options.modeSchema,
> prefix: options.prefix.concat([ii]),
> schema: local.schemaPItems(schema) ||
> schema.additionalItems,
> swaggerJson: options.swaggerJson
> });
> });
> // 5.3.2. maxItems
> test = typeof schema.maxItems !== 'number' || data.length <=
> schema.maxItems;
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'arrayMaxItems',
> prefix: options.prefix,
> schema: schema
> });
> // 5.3.3. minItems
> test = typeof schema.minItems !== 'number' || data.length >=
> schema.minItems;
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'arrayMinItems',
> prefix: options.prefix,
> schema: schema
> });
> // 5.3.4. uniqueItems
> test = !schema.uniqueItems || data.every(function (element) {
> tmp = element;
> return data.indexOf(element) === data.lastIndexOf(element);
> });
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'arrayUniqueItems',
> prefix: options.prefix,
> schema: schema,
> tmp: tmp
> });
> break;
> // 5.4. Validation keywords for objects
> case 'object':
> // 5.4.1. maxProperties
> test = typeof schema.maxProperties !== 'number' ||
> Object.keys(data).length <= schema.maxProperties;
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'objectMaxProperties',
> prefix: options.prefix,
> schema: schema
> });
> // 5.4.2. minProperties
> test = typeof schema.minProperties !== 'number' ||
> Object.keys(data).length >= schema.minProperties;
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'objectMinProperties',
> prefix: options.prefix,
> schema: schema
> });
> // 5.4.3. required
> local.normalizeValue('list', schema.required).forEach(function
> (key) {
> test = !local.isNullOrUndefined(data[key]);
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'objectRequired',
> key: key,
> prefix: options.prefix,
> schema: schema
> });
> });
> // 5.4.4. additionalProperties, properties and patternProperties
> Object.keys(data).forEach(function (key) {
> tmp = null;
> if (schema.properties && schema.properties[key]) {
> tmp = true;
> // recurse - schema.properties
> local.validateBySwaggerSchema({
> data: data[key],
> dataReadonlyRemove: [
> dataReadonlyRemove2,
> key,
> dataReadonlyRemove2[key]
> ],
> modeSchema: options.modeSchema,
> prefix: options.prefix.concat([key]),
> schema: schema.properties[key],
> swaggerJson: options.swaggerJson
> });
> }
> Object.keys(schema.patternProperties || {}).forEach(function
> (rgx) {
> if (new RegExp(rgx).test(key)) {
> tmp = true;
> // recurse - schema.patternProperties
> local.validateBySwaggerSchema({
> data: data[key],
> modeSchema: options.modeSchema,
> prefix: options.prefix.concat([key]),
> schema: schema.patternProperties[rgx],
> swaggerJson: options.swaggerJson
> });
> }
> });
> /*
> * validate
> * 5.4.4.4. If "additionalProperties" has boolean value false
> *
> * In this case, validation of the instance depends on the property set of
> * "properties" and "patternProperties". In this section, the property
> names of
> * "patternProperties" will be called regexes for convenience.
> *
> * The first step is to collect the following sets:
> *
> * s
> * The property set of the instance to validate.
> * p
> * The property set from "properties".
> * pp
> * The property set from "patternProperties".
> * Having collected these three sets, the process is as follows:
> *
> * remove from "s" all elements of "p", if any;
> * for each regex in "pp", remove all elements of "s" which this regex
> matches.
> * Validation of the instance succeeds if, after these two steps, set "s"
> is empty.
> */
> test = tmp || schema.additionalProperties !== false;
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'objectAdditionalProperties',
> key: key,
> prefix: options.prefix,
> schema: schema
> });
> // recurse - schema.additionalProperties
> local.validateBySwaggerSchema({
> data: data[key],
> modeSchema: options.modeSchema,
> prefix: options.prefix.concat([key]),
> schema: schema.additionalProperties,
> swaggerJson: options.swaggerJson
> });
> });
> // 5.4.5. dependencies
> Object.keys(schema.dependencies || {}).forEach(function (key) {
> if (local.isNullOrUndefined(data[key])) {
> return;
> }
> // 5.4.5.2.1. Schema dependencies
> // recurse - schema.dependencies
> local.validateBySwaggerSchema({
> data: data[key],
> modeSchema: options.modeSchema,
> prefix: options.prefix.concat([key]),
> schema: schema.dependencies[key],
> swaggerJson: options.swaggerJson
> });
> // 5.4.5.2.2. Property dependencies
> local.normalizeValue('list',
> schema.dependencies[key]).every(function (key2) {
> test = !local.isNullOrUndefined(data[key2]);
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'objectDependencies',
> key: key,
> key2: key2,
> prefix: options.prefix,
> schema: schema
> });
> });
> });
> break;
> }
> // 5.5. Validation keywords for any instance type
> // 5.5.1. enum
> tmp = schema.enum || (!options.modeSchema &&
> (local.schemaPItems(schema) || {}).enum);
> test = !tmp || (Array.isArray(data)
> ? data
> : [data]).every(function (element) {
> return tmp.indexOf(element) >= 0;
> });
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'itemEnum',
> prefix: options.prefix,
> schema: schema,
> tmp: tmp
> });
> // 5.5.2. type
> local.nop();
> // 5.5.3. allOf
> (schema.allOf || []).forEach(function (element) {
> // recurse - schema.allOf
> local.validateBySwaggerSchema({
> data: data,
> prefix: options.prefix,
> modeSchema: options.modeSchema,
> schema: element,
> swaggerJson: options.swaggerJson
> });
> });
> // 5.5.4. anyOf
> tmp = null;
> test = !schema.anyOf || schema.anyOf.some(function (element) {
> local.tryCatchOnError(function () {
> // recurse - schema.anyOf
> local.validateBySwaggerSchema({
> data: data,
> modeSchema: options.modeSchema,
> prefix: options.prefix,
> schema: element,
> swaggerJson: options.swaggerJson
> });
> return true;
> }, local.nop);
> tmp = tmp || local.utility2._debugTryCatchError;
> return !tmp;
> });
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'itemOneOf',
> prefix: options.prefix,
> schema: schema,
> tmp: tmp
> });
> // 5.5.5. oneOf
> tmp = !schema.oneOf
> ? 1
> : 0;
> (schema.oneOf || []).some(function (element) {
> local.tryCatchOnError(function () {
> // recurse - schema.oneOf
> local.validateBySwaggerSchema({
> data: data,
> modeSchema: options.modeSchema,
> prefix: options.prefix,
> schema: element,
> swaggerJson: options.swaggerJson
> });
> tmp += 1;
> }, local.nop);
> return tmp > 1;
> });
> test = tmp === 1;
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'itemOneOf',
> prefix: options.prefix,
> schema: schema,
> tmp: tmp
> });
> // 5.5.6. not
> test = !schema.not || !local.tryCatchOnError(function () {
> // recurse - schema.not
> local.validateBySwaggerSchema({
> data: data,
> modeSchema: options.modeSchema,
> prefix: options.prefix,
> schema: schema.not,
> swaggerJson: options.swaggerJson
> });
> return true;
> }, local.nop);
> local.throwSwaggerError(!test && {
> data: data,
> errorType: 'itemNot',
> prefix: options.prefix,
> schema: schema
> });
> // 5.5.7. definitions
> local.nop();
> // validate data.$ref
> if (schema === local.swaggerSchemaJson.definitions.jsonReference) {
> local.validateBySwaggerSchema({
> modeDereference: true,
> modeSchema: options.modeSchema,
> prefix: options.prefix,
> schema: data,
> swaggerJson: options.swaggerJson
> });
> }
> return schema;
> };
>
> ```
>
> ```javascript
> /*
> * output from running code inside browser-console
> */
> var mySchema = {
> required: ['myBoolean'],
> properties: {
> myArrayOfStrings: { items: { type: 'string' }, type: 'array' },
> myBoolean: { type: 'boolean' },
> myNumber: { type: 'number' },
> myString: { enum: ['hello world', 'bye world'], type: 'string' }
> }
> };
>
> undefined
>
> local.validateBySwaggerSchema({
> data: {
> myArrayOfStrings: ['foo', 'bar'],
> myBoolean: false,
> myString: 'hello world'
> },
> prefix: ['myData'],
> schema: mySchema
> });
>
> {required: Array(1), properties: {…}}
>
> local.validateBySwaggerSchema({
> data: {
> myArrayOfStrings: [1, 2],
> myBoolean: false
> },
> prefix: ['myData'],
> schema: mySchema
> });
>
> assets.utility2.rollup.js:26060 Uncaught Error: error.itemType - value
> myData["myArrayOfStrings"][0] = 1 is not a valid string
> at Object.local.throwSwaggerError (assets.utility2.rollup.js:26048)
> at Object.local.validateBySwaggerSchema
> (assets.utility2.rollup.js:27017)
> at assets.utility2.rollup.js:27097
> at Array.forEach (<anonymous>)
> at Object.local.validateBySwaggerSchema
> (assets.utility2.rollup.js:27095)
> at assets.utility2.rollup.js:27172
> at Array.forEach (<anonymous>)
> at Object.local.validateBySwaggerSchema
> (assets.utility2.rollup.js:27167)
> at <anonymous>:2:7
> local.throwSwaggerError @ assets.utility2.rollup.js:26048
> local.validateBySwaggerSchema @ assets.utility2.rollup.js:27017
> (anonymous) @ assets.utility2.rollup.js:27097
> local.validateBySwaggerSchema @ assets.utility2.rollup.js:27095
> (anonymous) @ assets.utility2.rollup.js:27172
> local.validateBySwaggerSchema @ assets.utility2.rollup.js:27167
> (anonymous) @ VM341:2
>
> local.validateBySwaggerSchema({
> data: {
> myArrayOfStrings: ['foo', 'bar'],
> myBoolean: null
> },
> prefix: ['myData'],
> schema: mySchema
> });
>
> assets.utility2.rollup.js:26060 Uncaught Error: error.objectRequired -
> object myData = {"myArrayOfStrings":["foo","bar"],"myBoolean":null} must
> have property "myBoolean"
> at Object.local.throwSwaggerError (assets.utility2.rollup.js:26048)
> at assets.utility2.rollup.js:27158
> at Array.forEach (<anonymous>)
> at Object.local.validateBySwaggerSchema
> (assets.utility2.rollup.js:27156)
> at <anonymous>:2:7
> local.throwSwaggerError @ assets.utility2.rollup.js:26048
> (anonymous) @ assets.utility2.rollup.js:27158
> local.validateBySwaggerSchema @ assets.utility2.rollup.js:27156
> (anonymous) @ VM343:2
>
> local.validateBySwaggerSchema({
> data: {
> myBoolean: false,
> myString: 'hello undefined'
> },
> prefix: ['myData'],
> schema: mySchema
> });
>
> assets.utility2.rollup.js:26060 Uncaught Error: error.itemEnum - string
> myData["myString"] = "hello undefined" can only have items from the list
> ["hello world","bye world"]
> at Object.local.throwSwaggerError (assets.utility2.rollup.js:26048)
> at Object.local.validateBySwaggerSchema
> (assets.utility2.rollup.js:27274)
> at assets.utility2.rollup.js:27172
> at Array.forEach (<anonymous>)
> at Object.local.validateBySwaggerSchema
> (assets.utility2.rollup.js:27167)
> at <anonymous>:2:7
> local.throwSwaggerError @ assets.utility2.rollup.js:26048
> local.validateBySwaggerSchema @ assets.utility2.rollup.js:27274
> (anonymous) @ assets.utility2.rollup.js:27172
> local.validateBySwaggerSchema @ assets.utility2.rollup.js:27167
> (anonymous) @ VM345:2
> ```
>
> On Feb 18, 2018, at 9:42 AM, Jordan Harband <ljharb at gmail.com> wrote:
>
> Your proposal is conceptually the same as a labelled break statement (ie,
> GOTO); if you want to follow the advice to avoid labels, I suspect it would
> apply to your proposal as well.
>
> On Sat, Feb 17, 2018 at 4:44 PM, 李白|字一日 <calidion at gmail.com> wrote:
>
>> you can simply put these value handler pairs into an array.
>>
>>
>> const a = [[0, function(){}], [1, function(){}]];
>> let i = 0;
>>
>> while (a[i++][0]) {
>> a[i - 1][1]()
>> break;
>> }
>>
>>
>> 2018-02-18 5:58 GMT+08:00 sagiv ben giat <sagiv.bengiat at gmail.com>:
>>
>>> > What kind of argument is that? ESlint isn't a JavaScript runtime, it
>>> is fully configurable, and I don't see how it's at all relevant.
>>>
>>> I know ESLint can be configured, it was just an example for how `label`
>>> statements are considered as poor design of code.
>>>
>>>
>>>
>>> *Sagiv B.G*
>>>
>>> _______________________________________________
>>> es-discuss mailing list
>>> es-discuss at mozilla.org
>>> https://mail.mozilla.org/listinfo/es-discuss
>>>
>>>
>>
>> _______________________________________________
>> es-discuss mailing list
>> es-discuss at mozilla.org
>> https://mail.mozilla.org/listinfo/es-discuss
>>
>>
> _______________________________________________
> es-discuss mailing list
> es-discuss at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss
>
>
> _______________________________________________
> es-discuss mailing list
> es-discuss at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20180218/517fe3c2/attachment-0001.html>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: Screen-Shot-2018-02-18-at-9.32.49-AM-compressor.png
Type: image/png
Size: 334732 bytes
Desc: not available
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20180218/517fe3c2/attachment-0001.png>
More information about the es-discuss
mailing list