Closed sambauers closed 1 year ago
Hello @sambauers Thanks for rising this issue.
What is called "accessor" in your message is what we call "getters".
Getters and setters support is the most recent feature of stamps. https://github.com/stampit-org/stamp-specification/releases
The stampit
does support getters out of the box.
The @stamp/it
does not yet.
Sorry about that!
@PopGoesTheWza the root cause in the difference of packages is that migration of @stamp/*
to TS was never completed. Hence, we didn't implement the getters in @stamp/it
.
What's the stage of TS migration? Can we publish the new version of everything?
@sambauers There is a bug in your test. I've pulled it out to this gist: https://gist.github.com/koresar/21bd323148b2645d1d91601d8e2a8361
You are creating only one closure in the beginning of the test. And then reusing the closured storedValue
for two stamp instances. This is the same as using global scope, just more localised. Here is the code for you to think of:
function createClosure() {
var storedValue = Math.random();
return () => storedValue;
}
const hasStringAccessorResult = createClosure();
let instance1 = hasStringAccessorResult();
let instance2 = hasStringAccessorResult();
console.log(` - result: ${instance1 === instance2 ? '🟢 pass' : '🔴 fail'}`)
// This should pass, because the `Math.ramdom()` was called only once.
What you are looking to do is probably this:
const HasStringAccessor = stampit({
deepConf: {
protectedProperties: []
},
statics: {
protectProperty(...names) {
return this.deepConf({ protectedProperties: names });
}
},
init(keyValueMap, { stamp }) {
const map = new Map(stamp.compose.deepConfiguration.protectedProperties.map(name => [name, undefined]));
for (const [key, value] of Object.entries(keyValueMap)) {
if (map.has(key)) map.set(key, value)
}
for (const name of map.keys()) {
Object.defineProperty(this, name, {
get () { return map.get(name); },
set (value) {
if (!["undefined", "string"].includes(typeof value)) return;
map.set(name, value);
}
});
}
}
});
And usage:
const TestAccessors = stampit(
HasStringAccessor.protectProperty("one", "two"),
{ name: "TestAccessors" }
);
const instance1 = TestAccessors({ one: "mango", two: "2" });
instance1.one = {BadBanana: true};
t.equal(instance1.one, "mango");
instance1.two = ["bad array"];
t.equal(instance1.two, "2");
const instance2 = TestAccessors({ one: "banana", two: "222" });
t.equal(instance2.one, "banana");
t.equal(instance2.two, "222");
// check instance1 again
t.equal(instance1.one, "mango");
t.equal(instance1.two, "2");
instance1.one = "1"
t.equal(instance1.one, "1");
instance2.one = "111"
t.equal(instance2.one, "111");
Right, so at this point I'm quite confused. I'm attempting to transition from OOP classes. So I just want to know what is the equivalent of a class property (the above discovery was part of my attempts to work that out). In an OOP class I can do:
class TestAccessors {
public _one: string | undefined // can be private too - doesn't matter
constructor(opts?: Record<string, string | undefined> = {}) {
this.one = opts.one
}
get one(): string | undefined {
return this._one
}
set one(value) {
if (!['undefined', 'string'].includes(typeof value)) return
this._one = value
}
}
const instance1 = new TestAccessors({ one: 'mango' })
console.log(Object.assign({}, instance1)) // { _one: 'mango' } as expected
const instance2 = new TestAccessors({ one: 'banana' })
console.log(Object.assign({}, instance2)) // { _one: 'banana' } as expected
console.log(Object.assign({}, instance1)) // still { _one: 'mango' } as expected
How do I get the same behaviour as shown here (localisation of property public _one
) with stamps? I know it is possible with weak maps and symbol keyed properties and the like but that all seems pretty hacky. Is the answer just to initialise values on properties in init
? That will still maintain a reference right? So updating one stamp instance's prop
will update all, right?
Or do I just use deepProps
?
And despite reading everything, I still can't work out the utility of conf
/deepConf
.
How do I get
Mate I wrote you the code how to get it. :)
I don't quite get: do you want to hide _one
or you don't?
Can you please write five line unit test of what you are trying to achieve?
(Asking 5 questions one after another does not help.)
About conf
and deepConf
. These are just arbitrary metadata. It is not used by stampit or stamps. It just exists to hold anything you want for whatever need you have.
I typically use it to collect information about how a stamp should behave. (Like, which properties to protect.)
I wrote an extended version of the stamp above. It can protect not only strings, but also any type of data.
Usage:
const _ = require("lodash");
const TestAccessors = stampit(
HasProtectedAccessor.protectProperty({
fullName: v => v === undefined || _.isString(v),
address: _.isArray,
}),
{ name: "TestAccessors" }
);
const instance1 = TestAccessors({ fullName: "John Johnson", address: [] });
instance1.fullName = 123.123; // WILL NOT WORK! Still "John Johnson"
instance1.fullName = undefined; // ok
instance1.address = 123.123; // WILL NOT WORK! Still []
instance1.address = undefined; // WILL NOT WORK! Still []
instance1.address = ["123 Acme St"]; // ok
Here is the full source code (also in this gist):
const HasProtectedAccessor = stampit({
// stamp configuration
deepConf: {
// collect all the property names to protect
protectedProperties: {}
},
statics: {
// API of the stamp
protectProperty(names) {
// will deep merge the base objects with `names`
return this.deepConf({ protectedProperties: names });
}
},
init(keyValueMap, { stamp }) {
// The properties to protect
let { protectedProperties } = stamp.compose.deepConfiguration || {};
// The closured map of protector functions
const protectors = new Map(Object.entries(protectedProperties)
.filter(([key, value]) => typeof value === "function"));
// The closured map of protected values
const map = new Map();
for (const key of protectors.keys()) map.set(key, undefined);
for (const name of map.keys()) {
// Using JS getters and setters to protect properties
Object.defineProperty(this, name, {
get () { return map.get(name); },
set (value) {
if (protectors.get(name)(value, name))
map.set(name, value);
}
});
}
// Initialise this object instance with the configured values
for (const [key, value] of Object.entries(keyValueMap)) {
if (map.has(key)) this[key] = value;
}
}
});
const _ = require("lodash");
const TestAccessors = stampit(
HasProtectedAccessor.protectProperty({
fullName: v => v === undefined || _.isString(v),
address: _.isArray,
}),
{ name: "TestAccessors" }
);
const instance1 = TestAccessors({ fullName: "John Johnson", address: [] });
t.strictEqual(instance1.fullName, "John Johnson");
t.deepEqual(instance1.address, []);
const instance2 = TestAccessors({ fullName: ["array"], address: "oops" }); // WILL NOT SET
t.strictEqual(instance2.fullName, undefined);
t.strictEqual(instance2.address, undefined);
instance2.fullName = function () {}; // WILL NOT SET
t.strictEqual(instance2.fullName, undefined);
instance2.address = { NotAnArray: 111 }; // WILL NOT SET
t.strictEqual(instance2.address, undefined);
instance2.fullName = "Andrew Anderson";
t.strictEqual(instance2.fullName, "Andrew Anderson");
instance2.address = ["111 Acme St", "London"];
t.deepEqual(instance2.address, ["111 Acme St", "London"]);
instance2.fullName = undefined;
t.strictEqual(instance2.fullName, undefined);
instance2.address = undefined; // WILL NOT SET
t.deepEqual(instance2.address, ["111 Acme St", "London"]);
I got a little excited and blogged about it :) https://medium.com/@koresar/fws-s2e3-generic-smart-setters-ec9cf2e0e997
Thank you Sam for the inspiration.
Thank you for taking the time to write a comprehensive solution. I had hoped there was a built-in way to solve this that I hadn't been able to see.
Sorry about my rapid-fire questions.
In the context of this issue (the different behaviour between packages) I didn't care if the property in the classic OOP class or it's stamp equivalent was public or private. I just wanted each instance of my class or stamp to start with default values, and not have the same reference to the stored value. I understand what you mean about the way I was trying to build a stamp effectively providing the same reference to both stamp instances. I'm inclined to either use props
or deepProps
with symbols as keys to store those values now, or a weak map on this
to make them truly hidden from sight.
Reading this blog post again - https://medium.com/@koresar/fun-with-stamps-episode-25-getters-and-setters-built-in-support-54ec74f5829b - I notice you are putting getters and setters on methods
. Is this preferable to props
? - e.g.
const buildStamp = (name) => {
const propName = `_${name}` // or Symbol(name) to stop direct access
return stampit({
props: {
[propName]: undefined
},
methods: {
get [name]() {
return this[propName]
},
set [name](value) {
this[propName] = value
}
}
})
}
Hello. Good to see you on track! Sorry for the late response. Busy as hell.
To automatically copy data (as in JS primitives) you should use deepProps
(and even props
if you don't have arrays or objects in there). It would copy data for you.
Also, it is always a good idea to have an initializer which copies data. Somehow, when people coming from classic classes, they struggle understanding the concept of having indefinite number of object initialisers. :)
BTW, I find confusing these two sentences:
I didn't care if the property ... was public or private
and
to make them truly hidden from sight
Sounds like you did care. :))) But that doesn't really matter.
Answering your question about putting getters and setters to methods
.
Having getters and setters in props
allows them to have closured private data (within the initialiser). But you copy each getter/setter per every new object instance.
Having getters and setters in methods
though is more memory friendly, because methods
is actually your object's prototype. Meaning, there will be only one getter/setter per multiple object instances created from the stamp.
I am seeing some differences in results when implementing stamps using these packages. This is causing me some confusion and even concern about the "right way" to do things.
It appears that the behaviour of
props
differs between these packages when creating a new instance of a stamp which has accessors in props.I'll provide an example in code:
Result when using
import stampit from '@stamp/it
:Result when using
import stampit from 'stampit'
:So it seems that
stampit
package deals with merging of accessors on props in a different way to@stamp/it
, and I'm not sure which is correct. My expectation (as per the code above) would be that they should behave like standard props and not be copied into the new instance.Note: you can ignore that I placed
let storedValueOne
in the global scope. This behaviour persists when the accessor stamp is built using a function. The only way to work around this behaviour is to place the stored value in another prop like_one
, or by using a weak map, but the point here is the divergence in behaviour between the packages.Here is some code using a "stamp builder" which encloses the stored value, which still fails when using
stampit
package: