diff --git a/CHANGELOG.md b/CHANGELOG.md index 34b600ed..e5f10125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to ## [Unreleased][unreleased] +- Implement privatize, private fields on prototypes: `privatize(instance)` + ## [2.2.0][] - 2020-07-10 ### Added diff --git a/README.md b/README.md index 2fa3add6..5d64d0cf 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ $ npm install @metarhia/common - [parseHost](#parsehosthost) - [override](#overrideobj-fn) - [mixin](#mixintarget-source) +- [privatize](#privatizeinstance) - [Pool](#class-pool) - [Pool.prototype.constructor](#poolprototypeconstructorfactory--null) - [Pool.prototype.get](#poolprototypeget) @@ -1334,6 +1335,20 @@ Previous function will be accessible by obj.fnName.inherited Mixin for ES6 classes without overriding existing methods +### privatize(instance) + +- `instance`: [``][object] source instance + +_Returns:_ [``][object] - destination instance + +Convert instance with public fields to instance with private fields + +_Example:_ + +```js +common.privatize({ private: 5, f() { return this.private; } }); +``` + ### class Pool #### Pool.prototype.constructor(factory = null) diff --git a/common.mjs b/common.mjs index 8e437a48..483328e2 100644 --- a/common.mjs +++ b/common.mjs @@ -90,6 +90,7 @@ export const localIPs = common.localIPs; export const parseHost = common.parseHost; export const override = common.override; export const mixin = common.mixin; +export const privatize = common.privatize; export const Pool = common.Pool; export const sortComparePriority = common.sortComparePriority; export const sortCompareDirectories = common.sortCompareDirectories; diff --git a/lib/oop.js b/lib/oop.js index 92e056e3..aca70f83 100644 --- a/lib/oop.js +++ b/lib/oop.js @@ -23,7 +23,36 @@ const mixin = (target, source) => { Object.assign(target, mix); }; +const ASCII_A = 65; + +// Convert instance with public fields to instance with private fields +// instance - , source instance +// Returns: - destination instance +// +// Example: common.privatize({ private: 5, f() { return this.private; } }); +const privatize = instance => { + const iface = {}; + const fields = Object.keys(instance); + for (const fieldName of fields) { + const field = instance[fieldName]; + if (typeof field === 'function') { + const boundMethod = field.bind(instance); + iface[fieldName] = boundMethod; + } else if (fieldName === fieldName.toUpperCase()) { + const first = fieldName.charCodeAt(0); + if (first >= ASCII_A) { + Object.defineProperty(iface, fieldName, { + enumerable: true, + get: () => field, + }); + } + } + } + return Object.freeze(iface); +}; + module.exports = { override, mixin, + privatize, }; diff --git a/test/oop.js b/test/oop.js index 6748246b..ac2ad971 100644 --- a/test/oop.js +++ b/test/oop.js @@ -62,3 +62,43 @@ metatests.test('multiple inheritance with mixin', test => { test.strictSame(obj.property5, 'from Child.method3'); test.end(); }); + +metatests.test('privatize', test => { + const source = { + CONSTANT: 'constant value', + field: 'field value', + '123': 'number field value', + counter: 0, + method() { + return [this.CONSTANT, this.field]; + }, + inc(n = 1) { + this.counter += n; + return this.counter; + }, + }; + const destination = common.privatize(source); + try { + destination.CONSTANT = 'can not change freezed'; + } catch (err) { + test.strictSame(err.constructor.name, 'TypeError'); + test.strictSame(destination.CONSTANT, 'constant value'); + } + test.strictSame(destination.CONSTANT, 'constant value'); + try { + destination.field = 'can not change freezed'; + } catch (err) { + test.strictSame(err.constructor.name, 'TypeError'); + test.strictSame(destination.field, undefined); + } + test.strictSame(destination['123'], undefined); + test.strictSame(destination.method(), ['constant value', 'field value']); + test.strictSame(destination.inc(), 1); + try { + destination.inc = () => 0; + } catch (err) { + test.strictSame(err.constructor.name, 'TypeError'); + test.strictSame(destination.inc(), 2); + } + test.end(); +});